diff --git a/src/library_async.js b/src/library_async.js index 9866b751008fb..5eb2a104aeae2 100644 --- a/src/library_async.js +++ b/src/library_async.js @@ -343,8 +343,8 @@ addToLibrary({ #endif Asyncify.state = Asyncify.State.Rewinding; runAndAbortIfError(() => _asyncify_start_rewind(Asyncify.currData)); - if (typeof Browser != 'undefined' && Browser.mainLoop.func) { - Browser.mainLoop.resume(); + if (typeof MainLoop != 'undefined' && MainLoop.func) { + MainLoop.resume(); } var asyncWasmReturnValue, isError = false; try { @@ -391,8 +391,8 @@ addToLibrary({ #if ASYNCIFY_DEBUG dbg(`ASYNCIFY: start unwind ${Asyncify.currData}`); #endif - if (typeof Browser != 'undefined' && Browser.mainLoop.func) { - Browser.mainLoop.pause(); + if (typeof MainLoop != 'undefined' && MainLoop.func) { + MainLoop.pause(); } runAndAbortIfError(() => _asyncify_start_unwind(Asyncify.currData)); } diff --git a/src/library_browser.js b/src/library_browser.js index b9f5f1a28e2b9..3b88f602a2edc 100644 --- a/src/library_browser.js +++ b/src/library_browser.js @@ -7,11 +7,9 @@ // Utilities for browser environments var LibraryBrowser = { $Browser__deps: [ - '$setMainLoop', '$callUserCallback', '$safeSetTimeout', '$warnOnce', - 'emscripten_set_main_loop_timing', #if FILESYSTEM '$preloadPlugins', #if MAIN_MODULE @@ -25,84 +23,13 @@ var LibraryBrowser = { #if ASSERTIONS Module["requestFullScreen"] = Browser.requestFullScreen; #endif - Module["requestAnimationFrame"] = Browser.requestAnimationFrame; Module["setCanvasSize"] = Browser.setCanvasSize; - Module["pauseMainLoop"] = Browser.mainLoop.pause; - Module["resumeMainLoop"] = Browser.mainLoop.resume; Module["getUserMedia"] = Browser.getUserMedia; Module["createContext"] = Browser.createContext; var preloadedImages = {}; var preloadedAudios = {};`, $Browser: { - mainLoop: { - running: false, - scheduler: null, - method: '', - // Each main loop is numbered with a ID in sequence order. Only one main - // loop can run at a time. This variable stores the ordinal number of the - // main loop that is currently allowed to run. All previous main loops - // will quit themselves. This is incremented whenever a new main loop is - // created. - currentlyRunningMainloop: 0, - // The main loop tick function that will be called at each iteration. - func: null, - // The argument that will be passed to the main loop. (of type void*) - arg: 0, - timingMode: 0, - timingValue: 0, - currentFrameNumber: 0, - queue: [], - pause() { - Browser.mainLoop.scheduler = null; - // Incrementing this signals the previous main loop that it's now become old, and it must return. - Browser.mainLoop.currentlyRunningMainloop++; - }, - resume() { - Browser.mainLoop.currentlyRunningMainloop++; - var timingMode = Browser.mainLoop.timingMode; - var timingValue = Browser.mainLoop.timingValue; - var func = Browser.mainLoop.func; - Browser.mainLoop.func = null; - // do not set timing and call scheduler, we will do it on the next lines - setMainLoop(func, 0, false, Browser.mainLoop.arg, true); - _emscripten_set_main_loop_timing(timingMode, timingValue); - Browser.mainLoop.scheduler(); - }, - updateStatus() { -#if expectToReceiveOnModule('setStatus') - if (Module['setStatus']) { - var message = Module['statusMessage'] || 'Please wait...'; - var remaining = Browser.mainLoop.remainingBlockers; - var expected = Browser.mainLoop.expectedBlockers; - if (remaining) { - if (remaining < expected) { - Module['setStatus'](`{message} ({expected - remaining}/{expected})`); - } else { - Module['setStatus'](message); - } - } else { - Module['setStatus'](''); - } - } -#endif - }, - runIter(func) { - if (ABORT) return; -#if expectToReceiveOnModule('preMainLoop') - if (Module['preMainLoop']) { - var preRet = Module['preMainLoop'](); - if (preRet === false) { - return; // |return false| skips a frame - } - } -#endif - callUserCallback(func); -#if expectToReceiveOnModule('postMainLoop') - Module['postMainLoop']?.(); -#endif - } - }, useWebGL: false, isFullscreen: false, pointerLock: false, @@ -402,41 +329,6 @@ var LibraryBrowser = { return true; }, - nextRAF: 0, - - fakeRequestAnimationFrame(func) { - // try to keep 60fps between calls to here - var now = Date.now(); - if (Browser.nextRAF === 0) { - Browser.nextRAF = now + 1000/60; - } else { - while (now + 2 >= Browser.nextRAF) { // fudge a little, to avoid timer jitter causing us to do lots of delay:0 - Browser.nextRAF += 1000/60; - } - } - var delay = Math.max(Browser.nextRAF - now, 0); - setTimeout(func, delay); - }, - - requestAnimationFrame(func) { - if (typeof requestAnimationFrame == 'function') { - requestAnimationFrame(func); - return; - } - var RAF = Browser.fakeRequestAnimationFrame; -#if LEGACY_VM_SUPPORT - if (typeof window != 'undefined') { - RAF = window['requestAnimationFrame'] || - window['mozRequestAnimationFrame'] || - window['webkitRequestAnimationFrame'] || - window['msRequestAnimationFrame'] || - window['oRequestAnimationFrame'] || - RAF; - } -#endif - RAF(func); - }, - // abort and pause-aware versions TODO: build main loop on top of this? safeSetTimeout(func, timeout) { @@ -445,13 +337,6 @@ var LibraryBrowser = { // See https://github.com/libsdl-org/SDL/pull/6304 return safeSetTimeout(func, timeout); }, - safeRequestAnimationFrame(func) { - {{{ runtimeKeepalivePush() }}} - return Browser.requestAnimationFrame(() => { - {{{ runtimeKeepalivePop() }}} - callUserCallback(func); - }); - }, getMimetype(name) { return { @@ -816,277 +701,17 @@ var LibraryBrowser = { document.body.appendChild(script); }, - // Runs natively in pthread, no __proxy needed. - emscripten_get_main_loop_timing: (mode, value) => { - if (mode) {{{ makeSetValue('mode', 0, 'Browser.mainLoop.timingMode', 'i32') }}}; - if (value) {{{ makeSetValue('value', 0, 'Browser.mainLoop.timingValue', 'i32') }}}; - }, - - // Runs natively in pthread, no __proxy needed. - emscripten_set_main_loop_timing: (mode, value) => { - Browser.mainLoop.timingMode = mode; - Browser.mainLoop.timingValue = value; - - if (!Browser.mainLoop.func) { -#if ASSERTIONS - err('emscripten_set_main_loop_timing: Cannot set timing mode for main loop since a main loop does not exist! Call emscripten_set_main_loop first to set one up.'); -#endif - return 1; // Return non-zero on failure, can't set timing mode when there is no main loop. - } - - if (!Browser.mainLoop.running) { - {{{ runtimeKeepalivePush() }}} - Browser.mainLoop.running = true; - } - if (mode == {{{ cDefs.EM_TIMING_SETTIMEOUT }}}) { - Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_setTimeout() { - var timeUntilNextTick = Math.max(0, Browser.mainLoop.tickStartTime + value - _emscripten_get_now())|0; - setTimeout(Browser.mainLoop.runner, timeUntilNextTick); // doing this each time means that on exception, we stop - }; - Browser.mainLoop.method = 'timeout'; - } else if (mode == {{{ cDefs.EM_TIMING_RAF }}}) { - Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_rAF() { - Browser.requestAnimationFrame(Browser.mainLoop.runner); - }; - Browser.mainLoop.method = 'rAF'; - } else if (mode == {{{ cDefs.EM_TIMING_SETIMMEDIATE}}}) { - if (typeof Browser.setImmediate == 'undefined') { - if (typeof setImmediate == 'undefined') { - // Emulate setImmediate. (note: not a complete polyfill, we don't emulate clearImmediate() to keep code size to minimum, since not needed) - var setImmediates = []; - var emscriptenMainLoopMessageId = 'setimmediate'; - /** @param {Event} event */ - var Browser_setImmediate_messageHandler = (event) => { - // When called in current thread or Worker, the main loop ID is structured slightly different to accommodate for --proxy-to-worker runtime listening to Worker events, - // so check for both cases. - if (event.data === emscriptenMainLoopMessageId || event.data.target === emscriptenMainLoopMessageId) { - event.stopPropagation(); - setImmediates.shift()(); - } - }; - addEventListener("message", Browser_setImmediate_messageHandler, true); - Browser.setImmediate = /** @type{function(function(): ?, ...?): number} */((func) => { - setImmediates.push(func); - if (ENVIRONMENT_IS_WORKER) { - Module['setImmediates'] ??= []; - Module['setImmediates'].push(func); - postMessage({target: emscriptenMainLoopMessageId}); // In --proxy-to-worker, route the message via proxyClient.js - } else postMessage(emscriptenMainLoopMessageId, "*"); // On the main thread, can just send the message to itself. - }); - } else { - Browser.setImmediate = setImmediate; - } - } - Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_setImmediate() { - Browser.setImmediate(Browser.mainLoop.runner); - }; - Browser.mainLoop.method = 'immediate'; - } - return 0; - }, - - emscripten_set_main_loop__deps: ['$setMainLoop'], - emscripten_set_main_loop: (func, fps, simulateInfiniteLoop) => { - var browserIterationFunc = {{{ makeDynCall('v', 'func') }}}; - setMainLoop(browserIterationFunc, fps, simulateInfiniteLoop); - }, - - // Runs natively in pthread, no __proxy needed. - $setMainLoop__deps: [ - 'emscripten_set_main_loop_timing', 'emscripten_get_now', -#if OFFSCREEN_FRAMEBUFFER - 'emscripten_webgl_commit_frame', -#endif -#if !MINIMAL_RUNTIME - '$maybeExit', -#endif - ], - $setMainLoop__docs: ` - /** - * @param {number=} arg - * @param {boolean=} noSetTiming - */`, - $setMainLoop: (browserIterationFunc, fps, simulateInfiniteLoop, arg, noSetTiming) => { -#if ASSERTIONS - assert(!Browser.mainLoop.func, 'emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters.'); -#endif - Browser.mainLoop.func = browserIterationFunc; - Browser.mainLoop.arg = arg; - - var thisMainLoopId = Browser.mainLoop.currentlyRunningMainloop; - function checkIsRunning() { - if (thisMainLoopId < Browser.mainLoop.currentlyRunningMainloop) { -#if RUNTIME_DEBUG - dbg('main loop exiting'); -#endif - {{{ runtimeKeepalivePop() }}} -#if !MINIMAL_RUNTIME - maybeExit(); -#endif - return false; - } - return true; - } - - // We create the loop runner here but it is not actually running until - // _emscripten_set_main_loop_timing is called (which might happen a - // later time). This member signifies that the current runner has not - // yet been started so that we can call runtimeKeepalivePush when it - // gets it timing set for the first time. - Browser.mainLoop.running = false; - Browser.mainLoop.runner = function Browser_mainLoop_runner() { - if (ABORT) return; - if (Browser.mainLoop.queue.length > 0) { - var start = Date.now(); - var blocker = Browser.mainLoop.queue.shift(); - blocker.func(blocker.arg); - if (Browser.mainLoop.remainingBlockers) { - var remaining = Browser.mainLoop.remainingBlockers; - var next = remaining%1 == 0 ? remaining-1 : Math.floor(remaining); - if (blocker.counted) { - Browser.mainLoop.remainingBlockers = next; - } else { - // not counted, but move the progress along a tiny bit - next = next + 0.5; // do not steal all the next one's progress - Browser.mainLoop.remainingBlockers = (8*remaining + next)/9; - } - } -#if RUNTIME_DEBUG - dbg(`main loop blocker "${blocker.name}" took '${Date.now() - start} ms`); //, left: ' + Browser.mainLoop.remainingBlockers); -#endif - Browser.mainLoop.updateStatus(); - - // catches pause/resume main loop from blocker execution - if (!checkIsRunning()) return; - - setTimeout(Browser.mainLoop.runner, 0); - return; - } - - // catch pauses from non-main loop sources - if (!checkIsRunning()) return; - - // Implement very basic swap interval control - Browser.mainLoop.currentFrameNumber = Browser.mainLoop.currentFrameNumber + 1 | 0; - if (Browser.mainLoop.timingMode == {{{ cDefs.EM_TIMING_RAF }}} && Browser.mainLoop.timingValue > 1 && Browser.mainLoop.currentFrameNumber % Browser.mainLoop.timingValue != 0) { - // Not the scheduled time to render this frame - skip. - Browser.mainLoop.scheduler(); - return; - } else if (Browser.mainLoop.timingMode == {{{ cDefs.EM_TIMING_SETTIMEOUT }}}) { - Browser.mainLoop.tickStartTime = _emscripten_get_now(); - } - - // Signal GL rendering layer that processing of a new frame is about to start. This helps it optimize - // VBO double-buffering and reduce GPU stalls. -#if FULL_ES2 || LEGACY_GL_EMULATION - GL.newRenderingFrameStarted(); -#endif - -#if PTHREADS && OFFSCREEN_FRAMEBUFFER && GL_SUPPORT_EXPLICIT_SWAP_CONTROL - // If the current GL context is a proxied regular WebGL context, and was initialized with implicit swap mode on the main thread, and we are on the parent thread, - // perform the swap on behalf of the user. - if (typeof GL != 'undefined' && GL.currentContext && GL.currentContextIsProxied) { - var explicitSwapControl = {{{ makeGetValue('GL.currentContext', 0, 'i32') }}}; - if (!explicitSwapControl) _emscripten_webgl_commit_frame(); - } -#endif - -#if OFFSCREENCANVAS_SUPPORT - // If the current GL context is an OffscreenCanvas, but it was initialized with implicit swap mode, perform the swap on behalf of the user. - if (typeof GL != 'undefined' && GL.currentContext && !GL.currentContextIsProxied && !GL.currentContext.attributes.explicitSwapControl && GL.currentContext.GLctx.commit) { - GL.currentContext.GLctx.commit(); - } -#endif - -#if ASSERTIONS - if (Browser.mainLoop.method === 'timeout' && Module.ctx) { - warnOnce('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!'); - Browser.mainLoop.method = ''; // just warn once per call to set main loop - } -#endif - - Browser.mainLoop.runIter(browserIterationFunc); - -#if STACK_OVERFLOW_CHECK - checkStackCookie(); -#endif - - // catch pauses from the main loop itself - if (!checkIsRunning()) return; - - // Queue new audio data. This is important to be right after the main loop invocation, so that we will immediately be able - // to queue the newest produced audio samples. - // TODO: Consider adding pre- and post- rAF callbacks so that GL.newRenderingFrameStarted() and SDL.audio.queueNewAudioData() - // do not need to be hardcoded into this function, but can be more generic. - if (typeof SDL == 'object') SDL.audio?.queueNewAudioData?.(); - - Browser.mainLoop.scheduler(); - } - - if (!noSetTiming) { - if (fps && fps > 0) { - _emscripten_set_main_loop_timing({{{ cDefs.EM_TIMING_SETTIMEOUT }}}, 1000.0 / fps); - } else { - // Do rAF by rendering each frame (no decimating) - _emscripten_set_main_loop_timing({{{ cDefs.EM_TIMING_RAF }}}, 1); - } - - Browser.mainLoop.scheduler(); - } - - if (simulateInfiniteLoop) { - throw 'unwind'; - } - }, - - // Runs natively in pthread, no __proxy needed. - emscripten_set_main_loop_arg__deps: ['$setMainLoop'], - emscripten_set_main_loop_arg: (func, arg, fps, simulateInfiniteLoop) => { - var browserIterationFunc = () => {{{ makeDynCall('vp', 'func') }}}(arg); - setMainLoop(browserIterationFunc, fps, simulateInfiniteLoop, arg); - }, - - // Runs natively in pthread, no __proxy needed. - emscripten_cancel_main_loop: () => { - Browser.mainLoop.pause(); - Browser.mainLoop.func = null; - }, - - // Runs natively in pthread, no __proxy needed. - emscripten_pause_main_loop: () => { - Browser.mainLoop.pause(); - }, - - // Runs natively in pthread, no __proxy needed. - emscripten_resume_main_loop: () => { - Browser.mainLoop.resume(); - }, - - // Runs natively in pthread, no __proxy needed. - _emscripten_push_main_loop_blocker: (func, arg, name) => { - Browser.mainLoop.queue.push({ func: () => { - {{{ makeDynCall('vp', 'func') }}}(arg); - }, name: UTF8ToString(name), counted: true }); - Browser.mainLoop.updateStatus(); - }, - - // Runs natively in pthread, no __proxy needed. - _emscripten_push_uncounted_main_loop_blocker: (func, arg, name) => { - Browser.mainLoop.queue.push({ func: () => { - {{{ makeDynCall('vp', 'func') }}}(arg); - }, name: UTF8ToString(name), counted: false }); - Browser.mainLoop.updateStatus(); - }, - - // Runs natively in pthread, no __proxy needed. - emscripten_set_main_loop_expected_blockers: (num) => { - Browser.mainLoop.expectedBlockers = num; - Browser.mainLoop.remainingBlockers = num; - Browser.mainLoop.updateStatus(); + $safeRequestAnimationFrame__deps: ['$MainLoop'], + $safeRequestAnimationFrame: (func) => { + {{{ runtimeKeepalivePush() }}} + return MainLoop.requestAnimationFrame(() => { + {{{ runtimeKeepalivePop() }}} + callUserCallback(func); + }); }, // Runs natively in pthread, no __proxy needed. - emscripten_async_call__deps: ['$safeSetTimeout'], + emscripten_async_call__deps: ['$safeSetTimeout', '$safeRequestAnimationFrame'], emscripten_async_call: (func, arg, millis) => { function wrapper() { {{{ makeDynCall('vp', 'func') }}}(arg); @@ -1100,7 +725,7 @@ var LibraryBrowser = { ) { safeSetTimeout(wrapper, millis); } else { - Browser.safeRequestAnimationFrame(wrapper); + safeRequestAnimationFrame(wrapper); } }, diff --git a/src/library_eventloop.js b/src/library_eventloop.js index d07d245d9a276..d6f2d14161e2e 100644 --- a/src/library_eventloop.js +++ b/src/library_eventloop.js @@ -148,6 +148,390 @@ LibraryJSEventLoop = { {{{ runtimeKeepalivePop() }}} clearInterval(id); }, + + $MainLoop__internal: true, + $MainLoop__deps: ['$setMainLoop', '$callUserCallback', 'emscripten_set_main_loop_timing'], + $MainLoop__postset: ` + Module["requestAnimationFrame"] = MainLoop.requestAnimationFrame; + Module["pauseMainLoop"] = MainLoop.pause; + Module["resumeMainLoop"] = MainLoop.resume;`, + $MainLoop: { + running: false, + scheduler: null, + method: '', + // Each main loop is numbered with a ID in sequence order. Only one main + // loop can run at a time. This variable stores the ordinal number of the + // main loop that is currently allowed to run. All previous main loops + // will quit themselves. This is incremented whenever a new main loop is + // created. + currentlyRunningMainloop: 0, + // The main loop tick function that will be called at each iteration. + func: null, + // The argument that will be passed to the main loop. (of type void*) + arg: 0, + timingMode: 0, + timingValue: 0, + currentFrameNumber: 0, + queue: [], + + pause() { + MainLoop.scheduler = null; + // Incrementing this signals the previous main loop that it's now become old, and it must return. + MainLoop.currentlyRunningMainloop++; + }, + + resume() { + MainLoop.currentlyRunningMainloop++; + var timingMode = MainLoop.timingMode; + var timingValue = MainLoop.timingValue; + var func = MainLoop.func; + MainLoop.func = null; + // do not set timing and call scheduler, we will do it on the next lines + setMainLoop(func, 0, false, MainLoop.arg, true); + _emscripten_set_main_loop_timing(timingMode, timingValue); + MainLoop.scheduler(); + }, + + updateStatus() { +#if expectToReceiveOnModule('setStatus') + if (Module['setStatus']) { + var message = Module['statusMessage'] || 'Please wait...'; + var remaining = MainLoop.remainingBlockers ?? 0; + var expected = MainLoop.expectedBlockers ?? 0; + if (remaining) { + if (remaining < expected) { + Module['setStatus'](`{message} ({expected - remaining}/{expected})`); + } else { + Module['setStatus'](message); + } + } else { + Module['setStatus'](''); + } + } +#endif + }, + + runIter(func) { + if (ABORT) return; +#if expectToReceiveOnModule('preMainLoop') + if (Module['preMainLoop']) { + var preRet = Module['preMainLoop'](); + if (preRet === false) { + return; // |return false| skips a frame + } + } +#endif + callUserCallback(func); +#if expectToReceiveOnModule('postMainLoop') + Module['postMainLoop']?.(); +#endif + }, + + nextRAF: 0, + + fakeRequestAnimationFrame(func) { + // try to keep 60fps between calls to here + var now = Date.now(); + if (MainLoop.nextRAF === 0) { + MainLoop.nextRAF = now + 1000/60; + } else { + while (now + 2 >= MainLoop.nextRAF) { // fudge a little, to avoid timer jitter causing us to do lots of delay:0 + MainLoop.nextRAF += 1000/60; + } + } + var delay = Math.max(MainLoop.nextRAF - now, 0); + setTimeout(func, delay); + }, + + requestAnimationFrame(func) { + if (typeof requestAnimationFrame == 'function') { + requestAnimationFrame(func); + return; + } + var RAF = MainLoop.fakeRequestAnimationFrame; +#if LEGACY_VM_SUPPORT + if (typeof window != 'undefined') { + RAF = window['requestAnimationFrame'] || + window['mozRequestAnimationFrame'] || + window['webkitRequestAnimationFrame'] || + window['msRequestAnimationFrame'] || + window['oRequestAnimationFrame'] || + RAF; + } +#endif + RAF(func); + }, + }, + + emscripten_get_main_loop_timing__deps: ['$MainLoop'], + emscripten_get_main_loop_timing: (mode, value) => { + if (mode) {{{ makeSetValue('mode', 0, 'MainLoop.timingMode', 'i32') }}}; + if (value) {{{ makeSetValue('value', 0, 'MainLoop.timingValue', 'i32') }}}; + }, + + emscripten_set_main_loop_timing__deps: ['$MainLoop'], + emscripten_set_main_loop_timing: (mode, value) => { + MainLoop.timingMode = mode; + MainLoop.timingValue = value; + + if (!MainLoop.func) { +#if ASSERTIONS + err('emscripten_set_main_loop_timing: Cannot set timing mode for main loop since a main loop does not exist! Call emscripten_set_main_loop first to set one up.'); +#endif + return 1; // Return non-zero on failure, can't set timing mode when there is no main loop. + } + + if (!MainLoop.running) { + {{{ runtimeKeepalivePush() }}} + MainLoop.running = true; + } + if (mode == {{{ cDefs.EM_TIMING_SETTIMEOUT }}}) { + MainLoop.scheduler = function MainLoop_scheduler_setTimeout() { + var timeUntilNextTick = Math.max(0, MainLoop.tickStartTime + value - _emscripten_get_now())|0; + setTimeout(MainLoop.runner, timeUntilNextTick); // doing this each time means that on exception, we stop + }; + MainLoop.method = 'timeout'; + } else if (mode == {{{ cDefs.EM_TIMING_RAF }}}) { + MainLoop.scheduler = function MainLoop_scheduler_rAF() { + MainLoop.requestAnimationFrame(MainLoop.runner); + }; + MainLoop.method = 'rAF'; + } else if (mode == {{{ cDefs.EM_TIMING_SETIMMEDIATE}}}) { + if (typeof MainLoop.setImmediate == 'undefined') { + if (typeof setImmediate == 'undefined') { + // Emulate setImmediate. (note: not a complete polyfill, we don't emulate clearImmediate() to keep code size to minimum, since not needed) + var setImmediates = []; + var emscriptenMainLoopMessageId = 'setimmediate'; + /** @param {Event} event */ + var MainLoop_setImmediate_messageHandler = (event) => { + // When called in current thread or Worker, the main loop ID is structured slightly different to accommodate for --proxy-to-worker runtime listening to Worker events, + // so check for both cases. + if (event.data === emscriptenMainLoopMessageId || event.data.target === emscriptenMainLoopMessageId) { + event.stopPropagation(); + setImmediates.shift()(); + } + }; + addEventListener("message", MainLoop_setImmediate_messageHandler, true); + MainLoop.setImmediate = /** @type{function(function(): ?, ...?): number} */((func) => { + setImmediates.push(func); + if (ENVIRONMENT_IS_WORKER) { + Module['setImmediates'] ??= []; + Module['setImmediates'].push(func); + postMessage({target: emscriptenMainLoopMessageId}); // In --proxy-to-worker, route the message via proxyClient.js + } else postMessage(emscriptenMainLoopMessageId, "*"); // On the main thread, can just send the message to itself. + }); + } else { + MainLoop.setImmediate = setImmediate; + } + } + MainLoop.scheduler = function MainLoop_scheduler_setImmediate() { + MainLoop.setImmediate(MainLoop.runner); + }; + MainLoop.method = 'immediate'; + } + return 0; + }, + + emscripten_set_main_loop__deps: ['$setMainLoop'], + emscripten_set_main_loop: (func, fps, simulateInfiniteLoop) => { + var iterFunc = {{{ makeDynCall('v', 'func') }}}; + setMainLoop(iterFunc, fps, simulateInfiniteLoop); + }, + + $setMainLoop__internal: true, + $setMainLoop__deps: [ + '$MainLoop', + 'emscripten_set_main_loop_timing', 'emscripten_get_now', +#if OFFSCREEN_FRAMEBUFFER + 'emscripten_webgl_commit_frame', +#endif +#if !MINIMAL_RUNTIME + '$maybeExit', +#endif + ], + $setMainLoop__docs: ` + /** + * @param {number=} arg + * @param {boolean=} noSetTiming + */`, + $setMainLoop: (iterFunc, fps, simulateInfiniteLoop, arg, noSetTiming) => { +#if ASSERTIONS + assert(!MainLoop.func, 'emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters.'); +#endif + MainLoop.func = iterFunc; + MainLoop.arg = arg; + + var thisMainLoopId = MainLoop.currentlyRunningMainloop; + function checkIsRunning() { + if (thisMainLoopId < MainLoop.currentlyRunningMainloop) { +#if RUNTIME_DEBUG + dbg('main loop exiting'); +#endif + {{{ runtimeKeepalivePop() }}} +#if !MINIMAL_RUNTIME + maybeExit(); +#endif + return false; + } + return true; + } + + // We create the loop runner here but it is not actually running until + // _emscripten_set_main_loop_timing is called (which might happen a + // later time). This member signifies that the current runner has not + // yet been started so that we can call runtimeKeepalivePush when it + // gets it timing set for the first time. + MainLoop.running = false; + MainLoop.runner = function MainLoop_runner() { + if (ABORT) return; + if (MainLoop.queue.length > 0) { + var start = Date.now(); + var blocker = MainLoop.queue.shift(); + blocker.func(blocker.arg); + if (MainLoop.remainingBlockers) { + var remaining = MainLoop.remainingBlockers; + var next = remaining%1 == 0 ? remaining-1 : Math.floor(remaining); + if (blocker.counted) { + MainLoop.remainingBlockers = next; + } else { + // not counted, but move the progress along a tiny bit + next = next + 0.5; // do not steal all the next one's progress + MainLoop.remainingBlockers = (8*remaining + next)/9; + } + } +#if RUNTIME_DEBUG + dbg(`main loop blocker "${blocker.name}" took '${Date.now() - start} ms`); //, left: ' + MainLoop.remainingBlockers); +#endif + MainLoop.updateStatus(); + + // catches pause/resume main loop from blocker execution + if (!checkIsRunning()) return; + + setTimeout(MainLoop.runner, 0); + return; + } + + // catch pauses from non-main loop sources + if (!checkIsRunning()) return; + + // Implement very basic swap interval control + MainLoop.currentFrameNumber = MainLoop.currentFrameNumber + 1 | 0; + if (MainLoop.timingMode == {{{ cDefs.EM_TIMING_RAF }}} && MainLoop.timingValue > 1 && MainLoop.currentFrameNumber % MainLoop.timingValue != 0) { + // Not the scheduled time to render this frame - skip. + MainLoop.scheduler(); + return; + } else if (MainLoop.timingMode == {{{ cDefs.EM_TIMING_SETTIMEOUT }}}) { + MainLoop.tickStartTime = _emscripten_get_now(); + } + + // Signal GL rendering layer that processing of a new frame is about to start. This helps it optimize + // VBO double-buffering and reduce GPU stalls. +#if FULL_ES2 || LEGACY_GL_EMULATION + GL.newRenderingFrameStarted(); +#endif + +#if PTHREADS && OFFSCREEN_FRAMEBUFFER && GL_SUPPORT_EXPLICIT_SWAP_CONTROL + // If the current GL context is a proxied regular WebGL context, and was initialized with implicit swap mode on the main thread, and we are on the parent thread, + // perform the swap on behalf of the user. + if (typeof GL != 'undefined' && GL.currentContext && GL.currentContextIsProxied) { + var explicitSwapControl = {{{ makeGetValue('GL.currentContext', 0, 'i32') }}}; + if (!explicitSwapControl) _emscripten_webgl_commit_frame(); + } +#endif + +#if OFFSCREENCANVAS_SUPPORT + // If the current GL context is an OffscreenCanvas, but it was initialized with implicit swap mode, perform the swap on behalf of the user. + if (typeof GL != 'undefined' && GL.currentContext && !GL.currentContextIsProxied && !GL.currentContext.attributes.explicitSwapControl && GL.currentContext.GLctx.commit) { + GL.currentContext.GLctx.commit(); + } +#endif + +#if ASSERTIONS + if (MainLoop.method === 'timeout' && Module.ctx) { + warnOnce('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!'); + MainLoop.method = ''; // just warn once per call to set main loop + } +#endif + + MainLoop.runIter(iterFunc); + +#if STACK_OVERFLOW_CHECK + checkStackCookie(); +#endif + + // catch pauses from the main loop itself + if (!checkIsRunning()) return; + + // Queue new audio data. This is important to be right after the main loop invocation, so that we will immediately be able + // to queue the newest produced audio samples. + // TODO: Consider adding pre- and post- rAF callbacks so that GL.newRenderingFrameStarted() and SDL.audio.queueNewAudioData() + // do not need to be hardcoded into this function, but can be more generic. + if (typeof SDL == 'object') SDL.audio?.queueNewAudioData?.(); + + MainLoop.scheduler(); + } + + if (!noSetTiming) { + if (fps && fps > 0) { + _emscripten_set_main_loop_timing({{{ cDefs.EM_TIMING_SETTIMEOUT }}}, 1000.0 / fps); + } else { + // Do rAF by rendering each frame (no decimating) + _emscripten_set_main_loop_timing({{{ cDefs.EM_TIMING_RAF }}}, 1); + } + + MainLoop.scheduler(); + } + + if (simulateInfiniteLoop) { + throw 'unwind'; + } + }, + + emscripten_set_main_loop_arg__deps: ['$setMainLoop'], + emscripten_set_main_loop_arg: (func, arg, fps, simulateInfiniteLoop) => { + var iterFunc = () => {{{ makeDynCall('vp', 'func') }}}(arg); + setMainLoop(iterFunc, fps, simulateInfiniteLoop, arg); + }, + + emscripten_cancel_main_loop__deps: ['$MainLoop'], + emscripten_cancel_main_loop: () => { + MainLoop.pause(); + MainLoop.func = null; + }, + + emscripten_pause_main_loop__deps: ['$MainLoop'], + emscripten_pause_main_loop: () => { + MainLoop.pause(); + }, + + emscripten_resume_main_loop__deps: ['$MainLoop'], + emscripten_resume_main_loop: () => { + MainLoop.resume(); + }, + + _emscripten_push_main_loop_blocker__deps: ['$MainLoop'], + _emscripten_push_main_loop_blocker: (func, arg, name) => { + MainLoop.queue.push({ func: () => { + {{{ makeDynCall('vp', 'func') }}}(arg); + }, name: UTF8ToString(name), counted: true }); + MainLoop.updateStatus(); + }, + + _emscripten_push_uncounted_main_loop_blocker__deps: ['$MainLoop'], + _emscripten_push_uncounted_main_loop_blocker: (func, arg, name) => { + MainLoop.queue.push({ func: () => { + {{{ makeDynCall('vp', 'func') }}}(arg); + }, name: UTF8ToString(name), counted: false }); + MainLoop.updateStatus(); + }, + + emscripten_set_main_loop_expected_blockers__deps: ['$MainLoop'], + emscripten_set_main_loop_expected_blockers: (num) => { + MainLoop.expectedBlockers = num; + MainLoop.remainingBlockers = num; + MainLoop.updateStatus(); + }, + }; addToLibrary(LibraryJSEventLoop); diff --git a/src/library_glfw.js b/src/library_glfw.js index 4485fa68824bc..1793263803366 100644 --- a/src/library_glfw.js +++ b/src/library_glfw.js @@ -84,6 +84,7 @@ var LibraryGLFW = { }, $GLFW__deps: ['emscripten_get_now', '$GL', '$Browser', '$GLFW_Window', + '$MainLoop', '$stringToNewUTF8', 'emscripten_set_window_title', #if FILESYSTEM @@ -725,13 +726,13 @@ var LibraryGLFW = { joys: {}, // glfw joystick data lastGamepadState: [], - lastGamepadStateFrame: null, // The integer value of Browser.mainLoop.currentFrameNumber of when the last gamepad state was produced. + lastGamepadStateFrame: null, // The integer value of MainLoop.currentFrameNumber of when the last gamepad state was produced. refreshJoysticks: () => { // Produce a new Gamepad API sample if we are ticking a new game frame, or if not using emscripten_set_main_loop() at all to drive animation. - if (Browser.mainLoop.currentFrameNumber !== GLFW.lastGamepadStateFrame || !Browser.mainLoop.currentFrameNumber) { + if (MainLoop.currentFrameNumber !== GLFW.lastGamepadStateFrame || !MainLoop.currentFrameNumber) { GLFW.lastGamepadState = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads || []); - GLFW.lastGamepadStateFrame = Browser.mainLoop.currentFrameNumber; + GLFW.lastGamepadStateFrame = MainLoop.currentFrameNumber; for (var joy = 0; joy < GLFW.lastGamepadState.length; ++joy) { var gamepad = GLFW.lastGamepadState[joy]; diff --git a/src/library_glut.js b/src/library_glut.js index 6bea94feb3905..e0cbfa8b8dbd8 100644 --- a/src/library_glut.js +++ b/src/library_glut.js @@ -617,12 +617,13 @@ var LibraryGLUT = { glutSwapBuffers: () => {}, glutPostRedisplay__proxy: 'sync', + glutPostRedisplay__deps: ['$MainLoop'], glutPostRedisplay: () => { if (GLUT.displayFunc && !GLUT.requestedAnimationFrame) { GLUT.requestedAnimationFrame = true; - Browser.requestAnimationFrame(() => { + MainLoop.requestAnimationFrame(() => { GLUT.requestedAnimationFrame = false; - Browser.mainLoop.runIter(() => {{{ makeDynCall('v', 'GLUT.displayFunc') }}}()); + MainLoop.runIter(() => {{{ makeDynCall('v', 'GLUT.displayFunc') }}}()); }); } }, diff --git a/src/library_openal.js b/src/library_openal.js index b846bea994496..446d7f2fb7878 100644 --- a/src/library_openal.js +++ b/src/library_openal.js @@ -11,7 +11,7 @@ var LibraryOpenAL = { // ** INTERNALS // ************************************************************************ - $AL__deps: ['$Browser'], + $AL__deps: ['$MainLoop'], $AL: { // ------------------------------------------------------ // -- Constants @@ -88,7 +88,7 @@ var LibraryOpenAL = { // If we are animating using the requestAnimationFrame method, then the main loop does not run when in the background. // To give a perfect glitch-free audio stop when switching from foreground to background, we need to avoid updating // audio altogether when in the background, so detect that case and kill audio buffer streaming if so. - if (Browser.mainLoop.timingMode === {{{ cDefs.EM_TIMING_RAF }}} && document['visibilityState'] != 'visible') { + if (MainLoop.timingMode === {{{ cDefs.EM_TIMING_RAF }}} && document['visibilityState'] != 'visible') { return; } @@ -105,7 +105,7 @@ var LibraryOpenAL = { // to OpenAL parameters, such as pitch, may require the web audio queue to be flushed and rescheduled. scheduleSourceAudio: (src, lookahead) => { // See comment on scheduleContextAudio above. - if (Browser.mainLoop.timingMode === {{{ cDefs.EM_TIMING_RAF }}} && document['visibilityState'] != 'visible') { + if (MainLoop.timingMode === {{{ cDefs.EM_TIMING_RAF }}} && document['visibilityState'] != 'visible') { return; } if (src.state !== {{{ cDefs.AL_PLAYING }}}) { diff --git a/src/library_sdl.js b/src/library_sdl.js index 63f7e5e8223cf..2ca906a6c9539 100644 --- a/src/library_sdl.js +++ b/src/library_sdl.js @@ -18,6 +18,7 @@ var LibrarySDL = { $SDL__deps: [ '$PATH', '$Browser', 'SDL_GetTicks', 'SDL_LockSurface', + '$MainLoop', // For makeCEvent(). '$intArrayFromString', // Many SDL functions depend on malloc/free @@ -789,10 +790,10 @@ var LibrarySDL = { event.preventDefault(); break; case 'unload': - if (Browser.mainLoop.runner) { + if (MainLoop.runner) { SDL.events.push(event); // Force-run a main event loop, since otherwise this event will never be caught! - Browser.mainLoop.runner(); + MainLoop.runner(); } return; case 'resize': @@ -3337,7 +3338,7 @@ var LibrarySDL = { SDL_GL_GetSwapInterval__proxy: 'sync', SDL_GL_GetSwapInterval: () => { - if (Browser.mainLoop.timingMode == {{{ cDefs.EM_TIMING_RAF }}}) return Browser.mainLoop.timingValue; + if (MainLoop.timingMode == {{{ cDefs.EM_TIMING_RAF }}}) return MainLoop.timingValue; else return 0; }, diff --git a/test/other/codesize/test_codesize_hello_O0.gzsize b/test/other/codesize/test_codesize_hello_O0.gzsize index 19a67a42b75e8..a00c8d8503f05 100644 --- a/test/other/codesize/test_codesize_hello_O0.gzsize +++ b/test/other/codesize/test_codesize_hello_O0.gzsize @@ -1 +1 @@ -7985 +7991 diff --git a/test/other/codesize/test_codesize_hello_O0.jssize b/test/other/codesize/test_codesize_hello_O0.jssize index 3bbbae6169a46..325879017161c 100644 --- a/test/other/codesize/test_codesize_hello_O0.jssize +++ b/test/other/codesize/test_codesize_hello_O0.jssize @@ -1 +1 @@ -21358 +21372 diff --git a/test/other/codesize/test_codesize_minimal_O0.gzsize b/test/other/codesize/test_codesize_minimal_O0.gzsize index d98fa9ec21416..3152e4729360a 100644 --- a/test/other/codesize/test_codesize_minimal_O0.gzsize +++ b/test/other/codesize/test_codesize_minimal_O0.gzsize @@ -1 +1 @@ -6535 +6542 diff --git a/test/other/codesize/test_codesize_minimal_O0.jssize b/test/other/codesize/test_codesize_minimal_O0.jssize index 3028632c3c90f..80da3e23d9bb1 100644 --- a/test/other/codesize/test_codesize_minimal_O0.jssize +++ b/test/other/codesize/test_codesize_minimal_O0.jssize @@ -1 +1 @@ -17508 +17522 diff --git a/test/other/test_unoptimized_code_size.js.size b/test/other/test_unoptimized_code_size.js.size index 4faf6f3583ef2..ab171da5f9e62 100644 --- a/test/other/test_unoptimized_code_size.js.size +++ b/test/other/test_unoptimized_code_size.js.size @@ -1 +1 @@ -54727 +54741 diff --git a/test/other/test_unoptimized_code_size_strict.js.size b/test/other/test_unoptimized_code_size_strict.js.size index 62ae1a25f7032..936cf6959b6c2 100644 --- a/test/other/test_unoptimized_code_size_strict.js.size +++ b/test/other/test_unoptimized_code_size_strict.js.size @@ -1 +1 @@ -53592 +53606 diff --git a/test/test_other.py b/test/test_other.py index 96abca1e0f27a..04c3dbc89bc93 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -6510,15 +6510,15 @@ def do(name, source, moar_opts): test(['-O3', '--closure=1', '-Wno-closure', '-sWASM=0'], 36000) test(['-O3', '--closure=2', '-Wno-closure', '-sWASM=0'], 33000) # might change now and then - def test_no_browser(self): - BROWSER_INIT = 'var Browser' + def test_no_main_loop(self): + MAINLOOP = 'var MainLoop' self.run_process([EMCC, test_file('hello_world.c')]) - self.assertNotContained(BROWSER_INIT, read_file('a.out.js')) + self.assertNotContained(MAINLOOP, read_file('a.out.js')) - # uses emscripten_set_main_loop, which needs Browser + # uses emscripten_set_main_loop, which needs MainLoop self.run_process([EMCC, test_file('browser_main_loop.c')]) - self.assertContained(BROWSER_INIT, read_file('a.out.js')) + self.assertContained(MAINLOOP, read_file('a.out.js')) def test_EXPORTED_RUNTIME_METHODS(self): def test(opts, has, not_has):