Skip to content

Commit

Permalink
Basic support for running Emscripten code as AudioWorklets
Browse files Browse the repository at this point in the history
  • Loading branch information
tklajnscek committed Oct 14, 2020
1 parent 90c8740 commit 5bf9bb3
Show file tree
Hide file tree
Showing 11 changed files with 1,803 additions and 54 deletions.
10 changes: 10 additions & 0 deletions emcc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2867,6 +2867,16 @@ def modularize():
define([], function() { return %(EXPORT_NAME)s; });
else if (typeof exports === 'object')
exports["%(EXPORT_NAME)s"] = %(EXPORT_NAME)s;
else if (typeof AudioWorkletGlobalScope === 'function')
globalThis["%(EXPORT_NAME)s"] = %(EXPORT_NAME)s;
''' % {
'EXPORT_NAME': shared.Settings.EXPORT_NAME
})
else:
if(not src.endswith(';')):
f.write(';') # This is needed to make it a valid JS file after appending the below
f.write('''if (typeof AudioWorkletGlobalScope === 'function')
globalThis["%(EXPORT_NAME)s"] = %(EXPORT_NAME)s;
''' % {
'EXPORT_NAME': shared.Settings.EXPORT_NAME
})
Expand Down
164 changes: 158 additions & 6 deletions src/library_pthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ var LibraryPThread = {
PThread.mainThreadFutex = _main_thread_futex;
#if ASSERTIONS
assert(PThread.mainThreadFutex > 0);
#endif
PThread.workletFutex = _worklet_futex;
#if ASSERTIONS
assert(PThread.workletFutex > 0);
#endif
},
// Maps pthread_t to pthread info objects
Expand Down Expand Up @@ -353,6 +357,11 @@ var LibraryPThread = {
worker.runPthread();
delete worker.runPthread;
}
} 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;
} else if (cmd === 'print') {
out('Thread ' + d['threadId'] + ': ' + d['text']);
} else if (cmd === 'printErr') {
Expand Down Expand Up @@ -388,7 +397,7 @@ var LibraryPThread = {
};

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 @@ -468,6 +477,75 @@ var LibraryPThread = {
while(performance.now() < t) {
;
}
},

// 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 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);
})
});
}
},

Expand Down Expand Up @@ -1171,8 +1249,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 worklets.
if (!ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_WORKLET) {
#if PTHREADS_PROFILING
PThread.setThreadStatusConditional(_pthread_self(), {{{ cDefine('EM_THREAD_STATUS_RUNNING') }}}, {{{ cDefine('EM_THREAD_STATUS_WAITFUTEX') }}});
#endif
Expand All @@ -1184,7 +1262,58 @@ var LibraryPThread = {
if (ret === 'not-equal') return -{{{ cDefine('EWOULDBLOCK') }}};
if (ret === 'ok') return 0;
throw 'Atomics.wait returned an unexpected value ' + ret;
} else {
} else if (ENVIRONMENT_IS_WORKLET) {
// Worklets use a simple busy loop becuase Atomics.wait is not available in worklets, so simulate it via busy spinning.
// First, check if the value is correct for us to wait on.
if (Atomics.load(HEAP32, addr >> 2) != val) {
return -{{{ cDefine('EWOULDBLOCK') }}};
}

var tNow = performance.now();
var tEnd = tNow + timeout;

#if PTHREADS_PROFILING
PThread.setThreadStatusConditional(_pthread_self(), {{{ cDefine('EM_THREAD_STATUS_RUNNING') }}}, {{{ cDefine('EM_THREAD_STATUS_WAITFUTEX') }}});
#endif
// All worklets use the same global address since they all run on the
// render thread. When zero, the worklet is not waiting on anything, and on
// nonzero, the contents of the address pointed by PThread.workletFutex
// tell which address the worklet is simulating its wait on.
#if ASSERTIONS
assert(PThread.workletFutex > 0);
#endif
var lastAddr = Atomics.exchange(HEAP32, PThread.workletFutex >> 2, addr);
#if ASSERTIONS
// We must not have already been waiting.
assert(lastAddr == 0);
#endif

while (1) {
// Check for a timeout.
tNow = performance.now();
if (tNow > tEnd) {
#if PTHREADS_PROFILING
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, PThread.workletFutex >> 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
// us to run, but (tragically) that happened just a bit too late.
assert(lastAddr == addr || lastAddr == 0);
#endif
return -{{{ cDefine('ETIMEDOUT') }}};
}

lastAddr = Atomics.load(HEAP32, PThread.workletFutex >> 2);
if (lastAddr != addr) {
// We were told to stop waiting, so stop.
break;
}
}
}
else {
// First, check if the value is correct for us to wait on.
if (Atomics.load(HEAP32, addr >> 2) != val) {
return -{{{ cDefine('EWOULDBLOCK') }}};
Expand Down Expand Up @@ -1324,7 +1453,7 @@ var LibraryPThread = {
// 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(!ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_WORKLET);
#endif
var loadedAddr = Atomics.compareExchange(HEAP32, PThread.mainThreadFutex >> 2, mainThreadWaitAddress, 0);
if (loadedAddr == mainThreadWaitAddress) {
Expand All @@ -1334,9 +1463,32 @@ var LibraryPThread = {
}
}

// See if a worklet 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 worklets before any workers, so
// this scheme does not adhere to real queue-based waiting.
#if ASSERTIONS
assert(PThread.workletFutex > 0);
#endif
var workletWaitAddress = Atomics.load(HEAP32, PThread.workletFutex >> 2);
var workletWoken = 0;
if (workletWaitAddress == addr) {
#if ASSERTIONS
// We only use workletFutex in worklets, where we cannot block while we wait.
// Therefore we should only see it set from other threads, and not in worklets
// themselves. In other words, a worklet must never try to wake itself up!
assert(!ENVIRONMENT_IS_WORKLET);
#endif
var loadedAddr = Atomics.compareExchange(HEAP32, PThread.workletFutex >> 2, workletWaitAddress, 0);
if (loadedAddr == workletWaitAddress) {
--count;
workletWoken = 1;
if (count <= 0) return 1;
}
}

// 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 + mainThreadWoken + workletWoken;
throw 'Atomics.notify returned an unexpected value ' + ret;
},

Expand Down
7 changes: 6 additions & 1 deletion src/shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,20 @@ var ENVIRONMENT_IS_WEB = {{{ ENVIRONMENT === 'web' }}};
var ENVIRONMENT_IS_WORKER = {{{ ENVIRONMENT === 'worker' }}};
var ENVIRONMENT_IS_NODE = {{{ ENVIRONMENT === 'node' }}};
var ENVIRONMENT_IS_SHELL = {{{ ENVIRONMENT === 'shell' }}};
var ENVIRONMENT_IS_WORKLET = {{{ 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_WORKLET = false;
ENVIRONMENT_IS_WEB = typeof window === 'object';
ENVIRONMENT_IS_WORKER = typeof importScripts === '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_WORKLET = typeof AudioWorkletGlobalScope === 'function';
#endif // ENVIRONMENT

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

setWindowTitle = function(title) { document.title = title };
} else if (ENVIRONMENT_IS_WORKLET) {
// Nothing for worklets!
} else
#endif // ENVIRONMENT_MAY_BE_WEB || ENVIRONMENT_MAY_BE_WORKER
{
Expand Down Expand Up @@ -395,7 +400,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_WORKLET, 'Pthreads do not work in this environment yet (need Web Workers, or an alternative to them)');
#endif // USE_PTHREADS
#endif // ASSERTIONS

Expand Down
7 changes: 4 additions & 3 deletions src/shell_minimal.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ 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_WORKLET = typeof AudioWorkletGlobalScope === 'function';
var ENVIRONMENT_IS_PTHREAD = ENVIRONMENT_IS_WORKER || ENVIRONMENT_IS_WORKLET;
#if MODULARIZE
if (ENVIRONMENT_IS_WORKER) {
if (ENVIRONMENT_IS_PTHREAD) {
var buffer = {{{EXPORT_NAME}}}.buffer;
var STACK_BASE = {{{EXPORT_NAME}}}.STACK_BASE;
var STACKTOP = {{{EXPORT_NAME}}}.STACKTOP;
Expand Down
5 changes: 3 additions & 2 deletions src/shell_pthreads.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
* SPDX-License-Identifier: MIT
*/

// Three configurations we can be running in:
// Configurations we can be running in:
// 1) We could be the application main() thread running in the main JS UI thread. (ENVIRONMENT_IS_WORKER == false and ENVIRONMENT_IS_PTHREAD == false)
// 2) We could be the application main() thread proxied to worker. (with Emscripten -s PROXY_TO_WORKER=1) (ENVIRONMENT_IS_WORKER == true, ENVIRONMENT_IS_PTHREAD == false)
// 3) We could be an application pthread running in a worker. (ENVIRONMENT_IS_WORKER == true and ENVIRONMENT_IS_PTHREAD == true)
// 4) We could be an application pthread running in an audio worklet. (ENVIRONMENT_IS_WORKLET == true)

// ENVIRONMENT_IS_PTHREAD=true will have been preset in worker.js. Make it false in the main runtime thread.
var ENVIRONMENT_IS_PTHREAD = Module['ENVIRONMENT_IS_PTHREAD'] || false;
var ENVIRONMENT_IS_PTHREAD = Module['ENVIRONMENT_IS_PTHREAD'] || ENVIRONMENT_IS_WORKLET;
if (ENVIRONMENT_IS_PTHREAD) {
// Grab imports from the pthread to local scope.
buffer = Module['buffer'];
Expand Down
Loading

0 comments on commit 5bf9bb3

Please sign in to comment.