Skip to content

Commit 7cc73a0

Browse files
authored
Merge pull request #4412 from juj/offscreencanvas_support
offscreencanvas_support
2 parents 0c48c33 + 53751b2 commit 7cc73a0

17 files changed

+519
-22
lines changed

src/library_browser.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,14 @@ var LibraryBrowser = {
11641164
GL.newRenderingFrameStarted();
11651165
#endif
11661166

1167+
#if OFFSCREENCANVAS_SUPPORT
1168+
// If the current GL context is an OffscreenCanvas, but it was initialized with implicit swap mode, perform the swap
1169+
// in behalf of the user.
1170+
if (typeof GL !== 'undefined' && GL.currentContext && !GL.currentContext.attributes.explicitSwapControl && GL.currentContext.GLctx.commit) {
1171+
GL.currentContext.GLctx.commit();
1172+
}
1173+
#endif
1174+
11671175
if (Browser.mainLoop.method === 'timeout' && Module.ctx) {
11681176
Module.printErr('Looks like you are rendering without using requestAnimationFrame for the main loop. You should use 0 for the frame rate in emscripten_set_main_loop in order to use requestAnimationFrame, as that can greatly improve your frame rates!');
11691177
Browser.mainLoop.method = ''; // just warn once per call to set main loop

src/library_gl.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var LibraryGL = {
2323
vaos: [],
2424
contexts: [],
2525
currentContext: null,
26+
offscreenCanvases: {}, // DOM ID -> OffscreenCanvas mappings of <canvas> elements that have their rendering control transferred to offscreen.
2627
#if USE_WEBGL2
2728
queries: [],
2829
samplers: [],
@@ -528,6 +529,7 @@ var LibraryGL = {
528529
var handle = GL.getNewId(GL.contexts);
529530
var context = {
530531
handle: handle,
532+
attributes: webGLContextAttributes,
531533
version: webGLContextAttributes.majorVersion,
532534
GLctx: ctx
533535
};

src/library_html5.js

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,6 +1769,7 @@ var LibraryJSEvents = {
17691769
{{{ makeSetValue('attributes', C_STRUCTS.EmscriptenWebGLContextAttributes.majorVersion, 1, 'i32') }}};
17701770
{{{ makeSetValue('attributes', C_STRUCTS.EmscriptenWebGLContextAttributes.minorVersion, 0, 'i32') }}};
17711771
{{{ makeSetValue('attributes', C_STRUCTS.EmscriptenWebGLContextAttributes.enableExtensionsByDefault, 1, 'i32') }}};
1772+
{{{ makeSetValue('attributes', C_STRUCTS.EmscriptenWebGLContextAttributes.explicitSwapControl, 0, 'i32') }}};
17721773
},
17731774

17741775
emscripten_webgl_create_context__deps: ['$GL'],
@@ -1785,13 +1786,47 @@ var LibraryJSEvents = {
17851786
contextAttributes.majorVersion = {{{ makeGetValue('attributes', C_STRUCTS.EmscriptenWebGLContextAttributes.majorVersion, 'i32') }}};
17861787
contextAttributes.minorVersion = {{{ makeGetValue('attributes', C_STRUCTS.EmscriptenWebGLContextAttributes.minorVersion, 'i32') }}};
17871788
var enableExtensionsByDefault = {{{ makeGetValue('attributes', C_STRUCTS.EmscriptenWebGLContextAttributes.enableExtensionsByDefault, 'i32') }}};
1789+
contextAttributes.explicitSwapControl = {{{ makeGetValue('attributes', C_STRUCTS.EmscriptenWebGLContextAttributes.explicitSwapControl, 'i32') }}};
17881790

1789-
if (!target) {
1790-
target = Module['canvas'];
1791+
target = target ? Pointer_stringify(target) : '#canvas';
1792+
var canvas;
1793+
if (target) {
1794+
if (target === '#canvas' && Module['canvas']) {
1795+
target = Module['canvas'].id;
1796+
}
1797+
canvas = GL.offscreenCanvases[target] || JSEvents.findEventTarget(target);
17911798
} else {
1792-
target = JSEvents.findEventTarget(target);
1799+
canvas = GL.offscreenCanvases[Module['canvas'].id] || Module['canvas'];
1800+
}
1801+
if (!canvas) {
1802+
#if GL_DEBUG
1803+
console.error('emscripten_webgl_create_context failed: Unknown target!');
1804+
#endif
1805+
return 0;
17931806
}
1794-
var contextHandle = GL.createContext(target, contextAttributes);
1807+
#if OFFSCREENCANVAS_SUPPORT
1808+
if (contextAttributes.explicitSwapControl) {
1809+
var supportsOffscreenCanvas = canvas.transferControlToOffscreen || (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas);
1810+
if (!supportsOffscreenCanvas) {
1811+
#if GL_DEBUG
1812+
console.error('emscripten_webgl_create_context failed: OffscreenCanvas is not supported!');
1813+
#endif
1814+
return 0;
1815+
}
1816+
if (canvas.transferControlToOffscreen) {
1817+
GL.offscreenCanvases[canvas.id] = canvas.transferControlToOffscreen();
1818+
GL.offscreenCanvases[canvas.id].id = canvas.id;
1819+
canvas = GL.offscreenCanvases[canvas.id];
1820+
}
1821+
}
1822+
#else
1823+
if (contextAttributes.explicitSwapControl) {
1824+
console.error('emscripten_webgl_create_context failed: explicitSwapControl is not supported, please rebuild with -s OFFSCREENCANVAS_SUPPORT=1 to enable targeting the experimental OffscreenCanvas specification!');
1825+
return 0;
1826+
}
1827+
#endif
1828+
1829+
var contextHandle = GL.createContext(canvas, contextAttributes);
17951830
return contextHandle;
17961831
},
17971832

@@ -1804,6 +1839,29 @@ var LibraryJSEvents = {
18041839
return GL.currentContext ? GL.currentContext.handle : 0;
18051840
},
18061841

1842+
emscripten_webgl_commit_frame: function() {
1843+
if (!GL.currentContext || !GL.currentContext.GLctx) {
1844+
#if GL_DEBUG
1845+
console.error('emscripten_webgl_commit_frame() failed: no GL context set current via emscripten_webgl_make_context_current()!');
1846+
#endif
1847+
return {{{ cDefine('EMSCRIPTEN_RESULT_INVALID_TARGET') }}};
1848+
}
1849+
if (!GL.currentContext.GLctx.commit) {
1850+
#if GL_DEBUG
1851+
console.error('emscripten_webgl_commit_frame() failed: OffscreenCanvas is not supported by the current GL context!');
1852+
#endif
1853+
return {{{ cDefine('EMSCRIPTEN_RESULT_NOT_SUPPORTED') }}};
1854+
}
1855+
if (!GL.currentContext.attributes.explicitSwapControl) {
1856+
#if GL_DEBUG
1857+
console.error('emscripten_webgl_commit_frame() cannot be called for canvases with implicit swap control mode!');
1858+
#endif
1859+
return {{{ cDefine('EMSCRIPTEN_RESULT_INVALID_TARGET') }}};
1860+
}
1861+
GL.currentContext.GLctx.commit();
1862+
return {{{ cDefine('EMSCRIPTEN_RESULT_SUCCESS') }}};
1863+
},
1864+
18071865
emscripten_webgl_destroy_context: function(contextHandle) {
18081866
GL.deleteContext(contextHandle);
18091867
},

src/library_pthread.js

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ var LibraryPThread = {
5050
Atomics.store(HEAPU32, (pthreadPtr + {{{ C_STRUCTS.pthread.profilerBlock }}} ) >> 2, profilerBlock);
5151

5252
// Zero fill contents at startup.
53-
for(var i = 0; i < {{{ C_STRUCTS.thread_profiler_block.__size__ }}}; i += 4) Atomics.store(HEAPU32, (profilerBlock + i) >> 2, 0);
53+
for (var i = 0; i < {{{ C_STRUCTS.thread_profiler_block.__size__ }}}; i += 4) Atomics.store(HEAPU32, (profilerBlock + i) >> 2, 0);
5454
Atomics.store(HEAPU32, (pthreadPtr + {{{ C_STRUCTS.thread_profiler_block.currentStatusStartTime }}} ) >> 2, performance.now());
5555
},
5656

@@ -149,6 +149,31 @@ var LibraryPThread = {
149149
__register_pthread_ptr(0, 0, 0); // Unregister the thread block also inside the asm.js scope.
150150
threadInfoStruct = 0;
151151
if (ENVIRONMENT_IS_PTHREAD) {
152+
// This worker no longer owns any WebGL OffscreenCanvases, so transfer them back to parent thread.
153+
var transferList = [];
154+
155+
#if OFFSCREENCANVAS_SUPPORT
156+
var offscreenCanvases = {};
157+
if (typeof GL !== 'undefined') {
158+
offscreenCanvases = GL.offscreenCanvases;
159+
GL.offscreenCanvases = {};
160+
}
161+
for (var i in offscreenCanvases) {
162+
if (offscreenCanvases[i]) transferList.push(offscreenCanvases[i]);
163+
}
164+
if (transferList.length > 0) {
165+
postMessage({
166+
targetThread: parentThreadId,
167+
cmd: 'objectTransfer',
168+
offscreenCanvases: offscreenCanvases,
169+
moduleCanvasId: Module['canvas'].id, // moduleCanvasId specifies which canvas is denoted via the "#canvas" shorthand.
170+
transferList: transferList
171+
}, transferList);
172+
}
173+
// And clear the OffscreenCanvases from lingering around in this Worker as well.
174+
delete Module['canvas'];
175+
#endif
176+
152177
postMessage({ cmd: 'exit' });
153178
}
154179
}
@@ -204,6 +229,18 @@ var LibraryPThread = {
204229
if (pthread.worker) pthread.worker.pthread = null;
205230
},
206231

232+
receiveObjectTransfer: function(data) {
233+
#if OFFSCREENCANVAS_SUPPORT
234+
if (typeof GL !== 'undefined') {
235+
for (var i in data.offscreenCanvases) {
236+
GL.offscreenCanvases[i] = data.offscreenCanvases[i];
237+
GL.offscreenCanvases[i].id = i; // https://bugzilla.mozilla.org/show_bug.cgi?id=1281909
238+
}
239+
if (!Module['canvas']) Module['canvas'] = GL.offscreenCanvases[data.moduleCanvasId];
240+
}
241+
#endif
242+
},
243+
207244
// Allocates the given amount of new web workers and stores them in the pool of unused workers.
208245
// onFinishedLoading: A callback function that will be called once all of the workers have been initialized and are
209246
// ready to host pthreads. Optional. This is used to mitigate bug https://bugzilla.mozilla.org/show_bug.cgi?id=1049079
@@ -222,6 +259,17 @@ var LibraryPThread = {
222259
var worker = new Worker(pthreadMainJs);
223260

224261
worker.onmessage = function(e) {
262+
// If this message is intended to a recipient that is not the main thread, forward it to the target thread.
263+
if (e.data.targetThread && e.data.targetThread != _pthread_self()) {
264+
var thread = PThread.pthreads[e.data.targetThread];
265+
if (thread) {
266+
thread.worker.postMessage(e.data, e.data.transferList);
267+
} else {
268+
console.error('Internal error! Worker sent a message "' + e.data.cmd + '" to target pthread ' + e.data.targetThread + ', but that thread no longer exists!');
269+
}
270+
return;
271+
}
272+
225273
if (e.data.cmd === 'processQueuedMainThreadWork') {
226274
// TODO: Must post message to main Emscripten thread in PROXY_TO_WORKER mode.
227275
_emscripten_main_thread_process_queued_calls();
@@ -242,16 +290,18 @@ var LibraryPThread = {
242290
Module['print']('Thread ' + e.data.threadId + ': ' + e.data.text);
243291
} else if (e.data.cmd === 'printErr') {
244292
Module['printErr']('Thread ' + e.data.threadId + ': ' + e.data.text);
245-
} else if (e.data.cmd == 'alert') {
293+
} else if (e.data.cmd === 'alert') {
246294
alert('Thread ' + e.data.threadId + ': ' + e.data.text);
247295
} else if (e.data.cmd === 'exit') {
248-
// todo
296+
// currently no-op
249297
} else if (e.data.cmd === 'cancelDone') {
250-
PThread.freeThreadData(worker.pthread);
251-
worker.pthread = undefined; // Detach the worker from the pthread object, and return it to the worker pool as an unused worker.
252-
PThread.unusedWorkerPool.push(worker);
253-
// TODO: Free if detached.
254-
PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker.pthread), 1); // Not a running Worker anymore.
298+
PThread.freeThreadData(worker.pthread);
299+
worker.pthread = undefined; // Detach the worker from the pthread object, and return it to the worker pool as an unused worker.
300+
PThread.unusedWorkerPool.push(worker);
301+
// TODO: Free if detached.
302+
PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker.pthread), 1); // Not a running Worker anymore.
303+
} else if (e.data.cmd === 'objectTransfer') {
304+
PThread.receiveObjectTransfer(e.data);
255305
} else {
256306
Module['printErr']("worker sent an unknown command " + e.data.cmd);
257307
}
@@ -375,9 +425,14 @@ var LibraryPThread = {
375425
arg: threadParams.arg,
376426
threadInfoStruct: threadParams.pthread_ptr,
377427
selfThreadId: threadParams.pthread_ptr, // TODO: Remove this since thread ID is now the same as the thread address.
428+
parentThreadId: threadParams.parent_pthread_ptr,
378429
stackBase: threadParams.stackBase,
379-
stackSize: threadParams.stackSize
380-
});
430+
stackSize: threadParams.stackSize,
431+
#if OFFSCREENCANVAS_SUPPORT
432+
moduleCanvasId: threadParams.moduleCanvasId,
433+
offscreenCanvases: threadParams.offscreenCanvases,
434+
#endif
435+
}, threadParams.transferList);
381436
},
382437

383438
#if USE_PTHREADS
@@ -406,8 +461,6 @@ var LibraryPThread = {
406461

407462
pthread_create__deps: ['_spawn_thread', 'pthread_getschedparam', 'pthread_self'],
408463
pthread_create: function(pthread_ptr, attr, start_routine, arg) {
409-
if (ENVIRONMENT_IS_PTHREAD) return _emscripten_sync_run_in_main_thread_4({{{ cDefine('EM_PROXIED_PTHREAD_CREATE') }}}, pthread_ptr, attr, start_routine, arg);
410-
411464
if (typeof SharedArrayBuffer === 'undefined') {
412465
Module['printErr']('Current environment does not support SharedArrayBuffer, pthreads are not available!');
413466
return {{{ cDefine('EAGAIN') }}};
@@ -416,6 +469,65 @@ var LibraryPThread = {
416469
Module['printErr']('pthread_create called with a null thread pointer!');
417470
return {{{ cDefine('EINVAL') }}};
418471
}
472+
473+
var transferList = []; // List of JS objects that will transfer ownership to the Worker hosting the thread
474+
475+
#if OFFSCREENCANVAS_SUPPORT
476+
// Deduce which WebGL canvases (HTMLCanvasElements or OffscreenCanvases) should be passed over to the
477+
// Worker that hosts the spawned pthread.
478+
var transferredCanvasNames = {{{ makeGetValue('attr', 36, 'i32') }}}; // Comma-delimited list of IDs "canvas1, canvas2, ..."
479+
if (transferredCanvasNames) transferredCanvasNames = Pointer_stringify(transferredCanvasNames).split(',');
480+
481+
var offscreenCanvases = {}; // Dictionary of OffscreenCanvas objects we'll transfer to the created thread to own
482+
var moduleCanvasId = Module['canvas'] ? Module['canvas'].id : '';
483+
for (var i in transferredCanvasNames) {
484+
var name = transferredCanvasNames[i].trim();
485+
var offscreenCanvas;
486+
try {
487+
if (name == '#canvas') {
488+
if (!Module['canvas']) {
489+
Module['printErr']('pthread_create: could not find canvas with ID "' + name + '" to transfer to thread!');
490+
return {{{ cDefine('EINVAL') }}};
491+
}
492+
name = Module['canvas'].id;
493+
}
494+
if (GL.offscreenCanvases[name]) {
495+
offscreenCanvas = GL.offscreenCanvases[name];
496+
GL.offscreenCanvases[name] = null; // This thread no longer owns this canvas.
497+
if (Module['canvas'] instanceof OffscreenCanvas && name === Module['canvas'].id) Module['canvas'] = null;
498+
} else {
499+
var canvas = (Module['canvas'] && Module['canvas'].id === name) ? Module['canvas'] : document.getElementByID(name);
500+
if (!canvas) {
501+
Module['printErr']('pthread_create: could not find canvas with ID "' + name + '" to transfer to thread!');
502+
return {{{ cDefine('EINVAL') }}};
503+
}
504+
if (canvas.controlTransferredOffscreen) {
505+
Module['printErr']('pthread_create: cannot transfer canvas with ID "' + name + '" to thread, since the current thread does not have control over it!');
506+
return {{{ cDefine('EPERM') }}}; // Operation not permitted, some other thread is accessing the canvas.
507+
}
508+
if (!canvas.transferControlToOffscreen) {
509+
Module['printErr']('pthread_create: cannot transfer control of canvas "' + name + '" to pthread, because current browser does not support OffscreenCanvas!');
510+
return {{{ cDefine('ENOSYS') }}}; // Function not implemented, browser doesn't have support for this.
511+
}
512+
offscreenCanvas = canvas.transferControlToOffscreen();
513+
canvas.controlTransferredOffscreen = true;
514+
offscreenCanvas.id = canvas.id;
515+
}
516+
transferList.push(offscreenCanvas);
517+
offscreenCanvases[offscreenCanvas.id] = offscreenCanvas;
518+
} catch(e) {
519+
Module['printErr']('pthread_create: failed to transfer control of canvas "' + name + '" to OffscreenCanvas! Error: ' + e);
520+
return {{{ cDefine('EINVAL') }}}; // Hitting this might indicate an implementation bug or some other internal error
521+
}
522+
}
523+
#endif
524+
525+
// Synchronously proxy the thread creation to main thread if possible. If we need to transfer ownership of objects, then
526+
// proxy asynchronously via postMessage.
527+
if (ENVIRONMENT_IS_PTHREAD && transferList.length == 0) {
528+
return _emscripten_sync_run_in_main_thread_4({{{ cDefine('EM_PROXIED_PTHREAD_CREATE') }}}, pthread_ptr, attr, start_routine, arg);
529+
}
530+
419531
var stackSize = 0;
420532
var stackBase = 0;
421533
var detached = 0; // Default thread attr is PTHREAD_CREATE_JOINABLE, i.e. start as not detached.
@@ -468,14 +580,20 @@ var LibraryPThread = {
468580
detached: detached,
469581
startRoutine: start_routine,
470582
pthread_ptr: threadInfoStruct,
583+
parent_pthread_ptr: _pthread_self(),
471584
arg: arg,
585+
#if OFFSCREENCANVAS_SUPPORT
586+
moduleCanvasId: moduleCanvasId,
587+
offscreenCanvases: offscreenCanvases,
588+
#endif
589+
transferList: transferList
472590
};
473591

474592
if (ENVIRONMENT_IS_PTHREAD) {
475593
// The prepopulated pool of web workers that can host pthreads is stored in the main JS thread. Therefore if a
476594
// pthread is attempting to spawn a new thread, the thread creation must be deferred to the main JS thread.
477595
threadParams.cmd = 'spawnThread';
478-
postMessage(threadParams);
596+
postMessage(threadParams, transferList);
479597
} else {
480598
// We are the main thread, so we have the pthread warmup pool in this thread and can fire off JS thread creation
481599
// directly ourselves.

src/pthread-main.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var buffer;
99
var threadInfoStruct = 0; // Info area for this thread in Emscripten HEAP (shared). If zero, this worker is not currently hosting an executing pthread.
1010

1111
var selfThreadId = 0; // The ID of this thread. 0 if not hosting a pthread.
12+
var parentThreadId = 0; // The ID of the parent pthread that launched this thread.
1213

1314
var tempDoublePtr = 0; // A temporary memory area for global float and double marshalling operations.
1415

@@ -54,12 +55,16 @@ this.onmessage = function(e) {
5455
importScripts(e.data.url);
5556
FS.createStandardStreams();
5657
postMessage({ cmd: 'loaded' });
58+
} else if (e.data.cmd === 'objectTransfer') {
59+
PThread.receiveObjectTransfer(e.data);
5760
} else if (e.data.cmd === 'run') { // This worker was idle, and now should start executing its pthread entry point.
5861
threadInfoStruct = e.data.threadInfoStruct;
5962
__register_pthread_ptr(threadInfoStruct, /*isMainBrowserThread=*/0, /*isMainRuntimeThread=*/0); // Pass the thread address inside the asm.js scope to store it for fast access that avoids the need for a FFI out.
6063
assert(threadInfoStruct);
6164
selfThreadId = e.data.selfThreadId;
65+
parentThreadId = e.data.parentThreadId;
6266
assert(selfThreadId);
67+
assert(parentThreadId);
6368
// TODO: Emscripten runtime has these variables twice(!), once outside the asm.js module, and a second time inside the asm.js module.
6469
// Review why that is? Can those get out of sync?
6570
STACK_BASE = STACKTOP = e.data.stackBase;
@@ -69,6 +74,8 @@ this.onmessage = function(e) {
6974
Runtime.establishStackSpace(e.data.stackBase, e.data.stackBase + e.data.stackSize);
7075
var result = 0;
7176

77+
PThread.receiveObjectTransfer(e.data);
78+
7279
PThread.setThreadStatus(_pthread_self(), 1/*EM_THREAD_STATUS_RUNNING*/);
7380

7481
try {
@@ -92,7 +99,8 @@ this.onmessage = function(e) {
9299
}
93100
// The thread might have finished without calling pthread_exit(). If so, then perform the exit operation ourselves.
94101
// (This is a no-op if explicit pthread_exit() had been called prior.)
95-
PThread.threadExit(result);
102+
if (!Module['noExitRuntime']) PThread.threadExit(result);
103+
else console.log('pthread noExitRuntime: not quitting.');
96104
} else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread.
97105
if (threadInfoStruct && PThread.thisThreadCancelState == 0/*PTHREAD_CANCEL_ENABLE*/) {
98106
PThread.threadCancel();

src/settings.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,4 +756,8 @@ var BUNDLED_CD_DEBUG_FILE = ""; // Path to the CyberDWARF debug file passed to t
756756

757757
var TEXTDECODER = 1; // Is enabled, use the JavaScript TextDecoder API for string marshalling.
758758
// Enabled by default, set this to 0 to disable.
759+
var OFFSCREENCANVAS_SUPPORT = 0; // If set to 1, enables support for transferring canvases to pthreads and creating WebGL contexts in them,
760+
// as well as explicit swap control for GL contexts. This needs browser support for the OffscreenCanvas
761+
// specification.
762+
759763
// Reserved: variables containing POINTER_MASKING.

src/struct_info.compiled.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)