[HTML5] Refactor JS, threads support, closures.
- Refactored the Engine code, splitted across files. - Use MODULARIZE option to build emscripten code into it's own closure. - Enable lto support (saves ~2MiB in release). - Enable optional closure compiler pass for JS and generated code. - Enable optional pthreads support. - Can now build with tools=yes (not much to see yet). - Dropped some deprecated code for older toolchains.
This commit is contained in:
184
platform/javascript/engine/engine.js
Normal file
184
platform/javascript/engine/engine.js
Normal file
@@ -0,0 +1,184 @@
|
||||
Function('return this')()['Engine'] = (function() {
|
||||
|
||||
var unloadAfterInit = true;
|
||||
var canvas = null;
|
||||
var resizeCanvasOnStart = false;
|
||||
var customLocale = 'en_US';
|
||||
var wasmExt = '.wasm';
|
||||
|
||||
var preloader = new Preloader();
|
||||
var loader = new Loader();
|
||||
var rtenv = null;
|
||||
|
||||
var executableName = '';
|
||||
var loadPath = '';
|
||||
var loadPromise = null;
|
||||
var initPromise = null;
|
||||
var stderr = null;
|
||||
var stdout = null;
|
||||
var progressFunc = null;
|
||||
|
||||
function load(basePath) {
|
||||
if (loadPromise == null) {
|
||||
loadPath = basePath;
|
||||
loadPromise = preloader.loadPromise(basePath + wasmExt);
|
||||
preloader.setProgressFunc(progressFunc);
|
||||
requestAnimationFrame(preloader.animateProgress);
|
||||
}
|
||||
return loadPromise;
|
||||
};
|
||||
|
||||
function unload() {
|
||||
loadPromise = null;
|
||||
};
|
||||
|
||||
/** @constructor */
|
||||
function Engine() {};
|
||||
|
||||
Engine.prototype.init = /** @param {string=} basePath */ function(basePath) {
|
||||
if (initPromise) {
|
||||
return initPromise;
|
||||
}
|
||||
if (!loadPromise) {
|
||||
if (!basePath) {
|
||||
initPromise = Promise.reject(new Error("A base path must be provided when calling `init` and the engine is not loaded."));
|
||||
return initPromise;
|
||||
}
|
||||
load(basePath);
|
||||
}
|
||||
var config = {}
|
||||
if (typeof stdout === 'function')
|
||||
config.print = stdout;
|
||||
if (typeof stderr === 'function')
|
||||
config.printErr = stderr;
|
||||
initPromise = loader.init(loadPromise, loadPath, config).then(function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
rtenv = loader.env;
|
||||
if (unloadAfterInit) {
|
||||
loadPromise = null;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return initPromise;
|
||||
};
|
||||
|
||||
/** @type {function(string, string):Object} */
|
||||
Engine.prototype.preloadFile = function(file, path) {
|
||||
return preloader.preload(file, path);
|
||||
};
|
||||
|
||||
/** @type {function(...string):Object} */
|
||||
Engine.prototype.start = function() {
|
||||
// Start from arguments.
|
||||
var args = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
args.push(arguments[i]);
|
||||
}
|
||||
var me = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
return me.init().then(function() {
|
||||
if (!(canvas instanceof HTMLCanvasElement)) {
|
||||
canvas = Utils.findCanvas();
|
||||
}
|
||||
rtenv['locale'] = customLocale;
|
||||
rtenv['canvas'] = canvas;
|
||||
rtenv['thisProgram'] = executableName;
|
||||
rtenv['resizeCanvasOnStart'] = resizeCanvasOnStart;
|
||||
loader.start(preloader.preloadedFiles, args).then(function() {
|
||||
loader = null;
|
||||
initPromise = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Engine.prototype.startGame = function(execName, mainPack) {
|
||||
// Start and init with execName as loadPath if not inited.
|
||||
executableName = execName;
|
||||
var me = this;
|
||||
return Promise.all([
|
||||
this.init(execName),
|
||||
this.preloadFile(mainPack, mainPack)
|
||||
]).then(function() {
|
||||
return me.start('--main-pack', mainPack);
|
||||
});
|
||||
};
|
||||
|
||||
Engine.prototype.setWebAssemblyFilenameExtension = function(override) {
|
||||
if (String(override).length === 0) {
|
||||
throw new Error('Invalid WebAssembly filename extension override');
|
||||
}
|
||||
wasmExt = String(override);
|
||||
};
|
||||
|
||||
Engine.prototype.setUnloadAfterInit = function(enabled) {
|
||||
unloadAfterInit = enabled;
|
||||
};
|
||||
|
||||
Engine.prototype.setCanvas = function(canvasElem) {
|
||||
canvas = canvasElem;
|
||||
};
|
||||
|
||||
Engine.prototype.setCanvasResizedOnStart = function(enabled) {
|
||||
resizeCanvasOnStart = enabled;
|
||||
};
|
||||
|
||||
Engine.prototype.setLocale = function(locale) {
|
||||
customLocale = locale;
|
||||
};
|
||||
|
||||
Engine.prototype.setExecutableName = function(newName) {
|
||||
executableName = newName;
|
||||
};
|
||||
|
||||
Engine.prototype.setProgressFunc = function(func) {
|
||||
progressFunc = func;
|
||||
}
|
||||
|
||||
Engine.prototype.setStdoutFunc = function(func) {
|
||||
|
||||
var print = function(text) {
|
||||
if (arguments.length > 1) {
|
||||
text = Array.prototype.slice.call(arguments).join(" ");
|
||||
}
|
||||
func(text);
|
||||
};
|
||||
if (rtenv)
|
||||
rtenv.print = print;
|
||||
stdout = print;
|
||||
};
|
||||
|
||||
Engine.prototype.setStderrFunc = function(func) {
|
||||
|
||||
var printErr = function(text) {
|
||||
if (arguments.length > 1)
|
||||
text = Array.prototype.slice.call(arguments).join(" ");
|
||||
func(text);
|
||||
};
|
||||
if (rtenv)
|
||||
rtenv.printErr = printErr;
|
||||
stderr = printErr;
|
||||
};
|
||||
|
||||
// Closure compiler exported engine methods.
|
||||
/** @export */
|
||||
Engine['isWebGLAvailable'] = Utils.isWebGLAvailable;
|
||||
Engine['load'] = load;
|
||||
Engine['unload'] = unload;
|
||||
Engine.prototype['init'] = Engine.prototype.init
|
||||
Engine.prototype['preloadFile'] = Engine.prototype.preloadFile
|
||||
Engine.prototype['start'] = Engine.prototype.start
|
||||
Engine.prototype['startGame'] = Engine.prototype.startGame
|
||||
Engine.prototype['setWebAssemblyFilenameExtension'] = Engine.prototype.setWebAssemblyFilenameExtension
|
||||
Engine.prototype['setUnloadAfterInit'] = Engine.prototype.setUnloadAfterInit
|
||||
Engine.prototype['setCanvas'] = Engine.prototype.setCanvas
|
||||
Engine.prototype['setCanvasResizedOnStart'] = Engine.prototype.setCanvasResizedOnStart
|
||||
Engine.prototype['setLocale'] = Engine.prototype.setLocale
|
||||
Engine.prototype['setExecutableName'] = Engine.prototype.setExecutableName
|
||||
Engine.prototype['setProgressFunc'] = Engine.prototype.setProgressFunc
|
||||
Engine.prototype['setStdoutFunc'] = Engine.prototype.setStdoutFunc
|
||||
Engine.prototype['setStderrFunc'] = Engine.prototype.setStderrFunc
|
||||
return Engine;
|
||||
})();
|
||||
3
platform/javascript/engine/externs.js
Normal file
3
platform/javascript/engine/externs.js
Normal file
@@ -0,0 +1,3 @@
|
||||
var Godot;
|
||||
var WebAssembly = {};
|
||||
WebAssembly.instantiate = function(buffer, imports) {};
|
||||
33
platform/javascript/engine/loader.js
Normal file
33
platform/javascript/engine/loader.js
Normal file
@@ -0,0 +1,33 @@
|
||||
var Loader = /** @constructor */ function() {
|
||||
|
||||
this.env = null;
|
||||
|
||||
this.init = function(loadPromise, basePath, config) {
|
||||
var me = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
var cfg = config || {};
|
||||
cfg['locateFile'] = Utils.createLocateRewrite(basePath);
|
||||
cfg['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise);
|
||||
loadPromise = null;
|
||||
Godot(cfg).then(function(module) {
|
||||
me.env = module;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.start = function(preloadedFiles, args) {
|
||||
var me = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (!me.env) {
|
||||
reject(new Error('The engine must be initialized before it can be started'));
|
||||
}
|
||||
preloadedFiles.forEach(function(file) {
|
||||
Utils.copyToFS(me.env['FS'], file.path, file.buffer);
|
||||
});
|
||||
preloadedFiles.length = 0; // Clear memory
|
||||
me.env['callMain'](args);
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
139
platform/javascript/engine/preloader.js
Normal file
139
platform/javascript/engine/preloader.js
Normal file
@@ -0,0 +1,139 @@
|
||||
var Preloader = /** @constructor */ function() {
|
||||
|
||||
var DOWNLOAD_ATTEMPTS_MAX = 4;
|
||||
var progressFunc = null;
|
||||
var lastProgress = { loaded: 0, total: 0 };
|
||||
|
||||
var loadingFiles = {};
|
||||
this.preloadedFiles = [];
|
||||
|
||||
function loadXHR(resolve, reject, file, tracker) {
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.open('GET', file);
|
||||
if (!file.endsWith('.js')) {
|
||||
xhr.responseType = 'arraybuffer';
|
||||
}
|
||||
['loadstart', 'progress', 'load', 'error', 'abort'].forEach(function(ev) {
|
||||
xhr.addEventListener(ev, onXHREvent.bind(xhr, resolve, reject, file, tracker));
|
||||
});
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function onXHREvent(resolve, reject, file, tracker, ev) {
|
||||
|
||||
if (this.status >= 400) {
|
||||
|
||||
if (this.status < 500 || ++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
||||
reject(new Error("Failed loading file '" + file + "': " + this.statusText));
|
||||
this.abort();
|
||||
return;
|
||||
} else {
|
||||
setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
switch (ev.type) {
|
||||
case 'loadstart':
|
||||
if (tracker[file] === undefined) {
|
||||
tracker[file] = {
|
||||
total: ev.total,
|
||||
loaded: ev.loaded,
|
||||
attempts: 0,
|
||||
final: false,
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'progress':
|
||||
tracker[file].loaded = ev.loaded;
|
||||
tracker[file].total = ev.total;
|
||||
break;
|
||||
|
||||
case 'load':
|
||||
tracker[file].final = true;
|
||||
resolve(this);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
||||
tracker[file].final = true;
|
||||
reject(new Error("Failed loading file '" + file + "'"));
|
||||
} else {
|
||||
setTimeout(loadXHR.bind(null, resolve, reject, file, tracker), 1000);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'abort':
|
||||
tracker[file].final = true;
|
||||
reject(new Error("Loading file '" + file + "' was aborted."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadPromise = function(file) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
loadXHR(resolve, reject, file, loadingFiles);
|
||||
});
|
||||
}
|
||||
|
||||
this.preload = function(pathOrBuffer, destPath) {
|
||||
if (pathOrBuffer instanceof ArrayBuffer) {
|
||||
pathOrBuffer = new Uint8Array(pathOrBuffer);
|
||||
} else if (ArrayBuffer.isView(pathOrBuffer)) {
|
||||
pathOrBuffer = new Uint8Array(pathOrBuffer.buffer);
|
||||
}
|
||||
if (pathOrBuffer instanceof Uint8Array) {
|
||||
this.preloadedFiles.push({
|
||||
path: destPath,
|
||||
buffer: pathOrBuffer
|
||||
});
|
||||
return Promise.resolve();
|
||||
} else if (typeof pathOrBuffer === 'string') {
|
||||
var me = this;
|
||||
return this.loadPromise(pathOrBuffer).then(function(xhr) {
|
||||
me.preloadedFiles.push({
|
||||
path: destPath || pathOrBuffer,
|
||||
buffer: xhr.response
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
} else {
|
||||
throw Promise.reject("Invalid object for preloading");
|
||||
}
|
||||
};
|
||||
|
||||
var animateProgress = function() {
|
||||
|
||||
var loaded = 0;
|
||||
var total = 0;
|
||||
var totalIsValid = true;
|
||||
var progressIsFinal = true;
|
||||
|
||||
Object.keys(loadingFiles).forEach(function(file) {
|
||||
const stat = loadingFiles[file];
|
||||
if (!stat.final) {
|
||||
progressIsFinal = false;
|
||||
}
|
||||
if (!totalIsValid || stat.total === 0) {
|
||||
totalIsValid = false;
|
||||
total = 0;
|
||||
} else {
|
||||
total += stat.total;
|
||||
}
|
||||
loaded += stat.loaded;
|
||||
});
|
||||
if (loaded !== lastProgress.loaded || total !== lastProgress.total) {
|
||||
lastProgress.loaded = loaded;
|
||||
lastProgress.total = total;
|
||||
if (typeof progressFunc === 'function')
|
||||
progressFunc(loaded, total);
|
||||
}
|
||||
if (!progressIsFinal)
|
||||
requestAnimationFrame(animateProgress);
|
||||
}
|
||||
this.animateProgress = animateProgress; // Also exposed to start it.
|
||||
|
||||
this.setProgressFunc = function(callback) {
|
||||
progressFunc = callback;
|
||||
}
|
||||
};
|
||||
69
platform/javascript/engine/utils.js
Normal file
69
platform/javascript/engine/utils.js
Normal file
@@ -0,0 +1,69 @@
|
||||
var Utils = {
|
||||
|
||||
createLocateRewrite: function(execName) {
|
||||
function rw(path) {
|
||||
if (path.endsWith('.worker.js')) {
|
||||
return execName + '.worker.js';
|
||||
} else if (path.endsWith('.js')) {
|
||||
return execName + '.js';
|
||||
} else if (path.endsWith('.wasm')) {
|
||||
return execName + '.wasm';
|
||||
}
|
||||
}
|
||||
return rw;
|
||||
},
|
||||
|
||||
createInstantiatePromise: function(wasmLoader) {
|
||||
function instantiateWasm(imports, onSuccess) {
|
||||
wasmLoader.then(function(xhr) {
|
||||
WebAssembly.instantiate(xhr.response, imports).then(function(result) {
|
||||
onSuccess(result['instance'], result['module']);
|
||||
});
|
||||
});
|
||||
wasmLoader = null;
|
||||
return {};
|
||||
};
|
||||
|
||||
return instantiateWasm;
|
||||
},
|
||||
|
||||
copyToFS: function(fs, path, buffer) {
|
||||
var p = path.lastIndexOf("/");
|
||||
var dir = "/";
|
||||
if (p > 0) {
|
||||
dir = path.slice(0, path.lastIndexOf("/"));
|
||||
}
|
||||
try {
|
||||
fs.stat(dir);
|
||||
} catch (e) {
|
||||
if (e.errno !== 44) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h
|
||||
throw e;
|
||||
}
|
||||
fs['mkdirTree'](dir);
|
||||
}
|
||||
// With memory growth, canOwn should be false.
|
||||
fs['writeFile'](path, new Uint8Array(buffer), {'flags': 'wx+'});
|
||||
},
|
||||
|
||||
findCanvas: function() {
|
||||
var nodes = document.getElementsByTagName('canvas');
|
||||
if (nodes.length && nodes[0] instanceof HTMLCanvasElement) {
|
||||
return nodes[0];
|
||||
}
|
||||
throw new Error("No canvas found");
|
||||
},
|
||||
|
||||
isWebGLAvailable: function(majorVersion = 1) {
|
||||
|
||||
var testContext = false;
|
||||
try {
|
||||
var testCanvas = document.createElement('canvas');
|
||||
if (majorVersion === 1) {
|
||||
testContext = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl');
|
||||
} else if (majorVersion === 2) {
|
||||
testContext = testCanvas.getContext('webgl2') || testCanvas.getContext('experimental-webgl2');
|
||||
}
|
||||
} catch (e) {}
|
||||
return !!testContext;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user