I hope this is deemed on-topic, as I don't have experience with emscripten directly, but a problem in WebGL applications made with Unity (which uses emscripten) has brought me here. I have already submitted a bug report on the Unity side which has since been accepted, but as there is no ETA for a fix from their side and I have WebGL content online that is affected, I'm trying to see if a workaround can be found / I can speed up that process. Since my WebGL builds have been online and untouched for a few months, but only recently started to show different behavior, I assume it's not simply a regression on the Unity side and thus something that could possibly be fixed in the code Unity emits for a WebGL build (as opposed to in Unity's internal code).
The "problem" is simple: I want to cap the frame rate at 30 frames per second, so as to not put unnecessary pressure on the GPU. This used to work fine, but for some time now old and new builds instead try to match the screen refresh rate (which often makes especially laptop fans spin up audibly). This doesn't give a good impression, especially since my content would look perfectly fine at 30 fps and my target audience may not have gaming-grade hardware.
I tried to look through the emscripten code Unity produces when building a WebGL app and added logging to isolate sections that might be of interest, but I'll link to the complete un-minified framework code Unity emits as well.
Here are the functions setMainLoop and _emscripten_set_main_loop_timing, which I suspect are relevant. I added comments in bold and red highlighting the order and content of the console log messages:
function _emscripten_set_main_loop_timing(mode, value) {
console.log("_emscripten_set_main_loop_timing called with " + mode + ", " + value); // 2: _emscripten_set_main_loop_timing called with 0, 33.333333333333336
Browser.mainLoop.timingMode = mode;
Browser.mainLoop.timingValue = value;
if (!Browser.mainLoop.func) {
return 1;
}
if (!Browser.mainLoop.running) {
Browser.mainLoop.running = true;
}
if (mode == 0) {
console.log("setting timeout"); // 3: called next
Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_setTimeout() {
var timeUntilNextTick = Math.max(0, Browser.mainLoop.tickStartTime + value - _emscripten_get_now()) | 0;
console.log("timeUntilNextTick: " + timeUntilNextTick); // 4: called regularly with timeUntilNextTick always around 32
setTimeout(Browser.mainLoop.runner, timeUntilNextTick);
};
Browser.mainLoop.method = "timeout";
} else if (mode == 1) {
Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_rAF() {
Browser.requestAnimationFrame(Browser.mainLoop.runner);
};
Browser.mainLoop.method = "rAF";
} else if (mode == 2) {
if (typeof setImmediate === "undefined") {
var setImmediates = [];
var emscriptenMainLoopMessageId = "setimmediate";
var Browser_setImmediate_messageHandler = function (event) {
if (event.data === emscriptenMainLoopMessageId || event.data.target === emscriptenMainLoopMessageId) {
event.stopPropagation();
setImmediates.shift()();
}
};
addEventListener("message", Browser_setImmediate_messageHandler, true);
setImmediate = function Browser_emulated_setImmediate(func) {
setImmediates.push(func);
if (ENVIRONMENT_IS_WORKER) {
if (Module["setImmediates"] === undefined) Module["setImmediates"] = [];
Module["setImmediates"].push(func);
postMessage({ target: emscriptenMainLoopMessageId });
} else postMessage(emscriptenMainLoopMessageId, "*");
};
}
Browser.mainLoop.scheduler = function Browser_mainLoop_scheduler_setImmediate() {
setImmediate(Browser.mainLoop.runner);
};
Browser.mainLoop.method = "immediate";
}
return 0;
}
function setMainLoop(browserIterationFunc, fps, simulateInfiniteLoop, arg, noSetTiming) {
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.");
Browser.mainLoop.func = browserIterationFunc;
Browser.mainLoop.arg = arg;
var thisMainLoopId = Browser.mainLoop.currentlyRunningMainloop;
function checkIsRunning() {
if (thisMainLoopId < Browser.mainLoop.currentlyRunningMainloop) {
maybeExit();
return false;
}
return true;
}
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 {
next = next + 0.5;
Browser.mainLoop.remainingBlockers = (8 * remaining + next) / 9;
}
}
console.log('main loop blocker "' + blocker.name + '" took ' + (Date.now() - start) + " ms");
Browser.mainLoop.updateStatus();
if (!checkIsRunning()) return;
console.log("Setting timeout to 0");
setTimeout(Browser.mainLoop.runner, 0);
return;
}
if (!checkIsRunning()) return;
Browser.mainLoop.currentFrameNumber = (Browser.mainLoop.currentFrameNumber + 1) | 0;
if (Browser.mainLoop.timingMode == 1 && Browser.mainLoop.timingValue > 1 && Browser.mainLoop.currentFrameNumber % Browser.mainLoop.timingValue != 0) {
console.log("Browser.mainLoop.timingMode == 1");
Browser.mainLoop.scheduler();
return;
} else if (Browser.mainLoop.timingMode == 0) {
console.log("Browser.mainLoop.timingMode == 0");
Browser.mainLoop.tickStartTime = _emscripten_get_now();
}
GL.newRenderingFrameStarted();
Browser.mainLoop.runIter(browserIterationFunc);
if (!checkIsRunning()) return;
if (typeof SDL === "object" && SDL.audio && SDL.audio.queueNewAudioData) SDL.audio.queueNewAudioData();
Browser.mainLoop.scheduler();
};
if (!noSetTiming) {
console.log("fps: " + fps); // 1: called once with fps: 0
fps = 30; // manually added in case that was the problem, it's not
if (fps && fps > 0) _emscripten_set_main_loop_timing(0, 1e3 / fps);
else _emscripten_set_main_loop_timing(1, 1);
Browser.mainLoop.scheduler();
}
if (simulateInfiniteLoop) {
throw "unwind";
}
}
Here's a link to a pastebin with those functions, in case the formatting is bad:
And the whole framework code (minus some unrelated obfuscated stuff to stay below pastebin's 512KB limit):
The values set for Browser.mainLoop.timingMode and Browser.mainLoop.timingValue and the resulting timeUntilNextTick all look right to me.
Is it possible that browser updates or maybe some dynamically loaded code have changed in the last months to make something here no longer work as it used to? The behavior is the same in Edge, Brave and Firefox.
My hope is that someone with emscripten / browser shenanigans experience has an idea what changed or how I could get back to a limited frame rate. I appreciate any insights!