11ab7b6efc
Previously WebAssembly.compile was used along with the secondary WebAssembly.instantiate overload. Using only the primary overload is recommended to get best performance.
367 lines
9.2 KiB
JavaScript
367 lines
9.2 KiB
JavaScript
return Module;
|
|
},
|
|
};
|
|
|
|
(function() {
|
|
var engine = Engine;
|
|
|
|
var USING_WASM = engine.USING_WASM;
|
|
var DOWNLOAD_ATTEMPTS_MAX = 4;
|
|
|
|
var basePath = null;
|
|
var engineLoadPromise = null;
|
|
|
|
var loadingFiles = {};
|
|
|
|
function getBasePath(path) {
|
|
|
|
if (path.endsWith('/'))
|
|
path = path.slice(0, -1);
|
|
if (path.lastIndexOf('.') > path.lastIndexOf('/'))
|
|
path = path.slice(0, path.lastIndexOf('.'));
|
|
return path;
|
|
}
|
|
|
|
function getBaseName(path) {
|
|
|
|
path = getBasePath(path);
|
|
return path.slice(path.lastIndexOf('/') + 1);
|
|
}
|
|
|
|
Engine = function Engine() {
|
|
|
|
this.rtenv = null;
|
|
|
|
var gameInitPromise = null;
|
|
var unloadAfterInit = true;
|
|
var memorySize = 268435456;
|
|
|
|
var progressFunc = null;
|
|
var pckProgressTracker = {};
|
|
var lastProgress = { loaded: 0, total: 0 };
|
|
|
|
var canvas = null;
|
|
var stdout = null;
|
|
var stderr = null;
|
|
|
|
this.initGame = function(mainPack) {
|
|
|
|
if (!gameInitPromise) {
|
|
|
|
if (mainPack === undefined) {
|
|
if (basePath !== null) {
|
|
mainPack = basePath + '.pck';
|
|
} else {
|
|
return Promise.reject(new Error("No main pack to load specified"));
|
|
}
|
|
}
|
|
if (basePath === null)
|
|
basePath = getBasePath(mainPack);
|
|
|
|
gameInitPromise = Engine.initEngine().then(
|
|
instantiate.bind(this)
|
|
);
|
|
var gameLoadPromise = loadPromise(mainPack, pckProgressTracker).then(function(xhr) { return xhr.response; });
|
|
gameInitPromise = Promise.all([gameLoadPromise, gameInitPromise]).then(function(values) {
|
|
// resolve with pck
|
|
return new Uint8Array(values[0]);
|
|
});
|
|
if (unloadAfterInit)
|
|
gameInitPromise.then(Engine.unloadEngine);
|
|
requestAnimationFrame(animateProgress);
|
|
}
|
|
return gameInitPromise;
|
|
};
|
|
|
|
function instantiate(initializer) {
|
|
|
|
var rtenvOpts = {
|
|
noInitialRun: true,
|
|
thisProgram: getBaseName(basePath),
|
|
engine: this,
|
|
};
|
|
if (typeof stdout === 'function')
|
|
rtenvOpts.print = stdout;
|
|
if (typeof stderr === 'function')
|
|
rtenvOpts.printErr = stderr;
|
|
if (typeof WebAssembly === 'object' && initializer instanceof ArrayBuffer) {
|
|
rtenvOpts.instantiateWasm = function(imports, onSuccess) {
|
|
WebAssembly.instantiate(initializer, imports).then(function(result) {
|
|
onSuccess(result.instance);
|
|
});
|
|
return {};
|
|
};
|
|
} else if (initializer.asm && initializer.mem) {
|
|
rtenvOpts.asm = initializer.asm;
|
|
rtenvOpts.memoryInitializerRequest = initializer.mem;
|
|
rtenvOpts.TOTAL_MEMORY = memorySize;
|
|
} else {
|
|
throw new Error("Invalid initializer");
|
|
}
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
rtenvOpts.onRuntimeInitialized = resolve;
|
|
rtenvOpts.onAbort = reject;
|
|
rtenvOpts.engine.rtenv = Engine.RuntimeEnvironment(rtenvOpts);
|
|
});
|
|
}
|
|
|
|
this.start = function(mainPack) {
|
|
|
|
return this.initGame(mainPack).then(synchronousStart.bind(this));
|
|
};
|
|
|
|
function synchronousStart(pckView) {
|
|
// TODO don't expect canvas when runninng as cli tool
|
|
if (canvas instanceof HTMLCanvasElement) {
|
|
this.rtenv.canvas = canvas;
|
|
} else {
|
|
var firstCanvas = document.getElementsByTagName('canvas')[0];
|
|
if (firstCanvas instanceof HTMLCanvasElement) {
|
|
this.rtenv.canvas = firstCanvas;
|
|
} else {
|
|
throw new Error("No canvas found");
|
|
}
|
|
}
|
|
|
|
var actualCanvas = this.rtenv.canvas;
|
|
var context = false;
|
|
try {
|
|
context = actualCanvas.getContext('webgl2') || actualCanvas.getContext('experimental-webgl2');
|
|
} catch (e) {}
|
|
if (!context) {
|
|
throw new Error("WebGL 2 not available");
|
|
}
|
|
|
|
// canvas can grab focus on click
|
|
if (actualCanvas.tabIndex < 0) {
|
|
actualCanvas.tabIndex = 0;
|
|
}
|
|
// necessary to calculate cursor coordinates correctly
|
|
actualCanvas.style.padding = 0;
|
|
actualCanvas.style.borderWidth = 0;
|
|
actualCanvas.style.borderStyle = 'none';
|
|
// until context restoration is implemented
|
|
actualCanvas.addEventListener('webglcontextlost', function(ev) {
|
|
alert("WebGL context lost, please reload the page");
|
|
ev.preventDefault();
|
|
}, false);
|
|
|
|
this.rtenv.FS.createDataFile('/', this.rtenv.thisProgram + '.pck', pckView, true, true, true);
|
|
gameInitPromise = null;
|
|
this.rtenv.callMain();
|
|
}
|
|
|
|
this.setProgressFunc = function(func) {
|
|
progressFunc = func;
|
|
};
|
|
|
|
function animateProgress() {
|
|
|
|
var loaded = 0;
|
|
var total = 0;
|
|
var totalIsValid = true;
|
|
var progressIsFinal = true;
|
|
|
|
[loadingFiles, pckProgressTracker].forEach(function(tracker) {
|
|
Object.keys(tracker).forEach(function(file) {
|
|
if (!tracker[file].final)
|
|
progressIsFinal = false;
|
|
if (!totalIsValid || tracker[file].total === 0) {
|
|
totalIsValid = false;
|
|
total = 0;
|
|
} else {
|
|
total += tracker[file].total;
|
|
}
|
|
loaded += tracker[file].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.setCanvas = function(elem) {
|
|
canvas = elem;
|
|
};
|
|
|
|
this.setAsmjsMemorySize = function(size) {
|
|
memorySize = size;
|
|
};
|
|
|
|
this.setUnloadAfterInit = function(enabled) {
|
|
|
|
if (enabled && !unloadAfterInit && gameInitPromise) {
|
|
gameInitPromise.then(Engine.unloadEngine);
|
|
}
|
|
unloadAfterInit = enabled;
|
|
};
|
|
|
|
this.setStdoutFunc = function(func) {
|
|
|
|
var print = function(text) {
|
|
if (arguments.length > 1) {
|
|
text = Array.prototype.slice.call(arguments).join(" ");
|
|
}
|
|
func(text);
|
|
};
|
|
if (this.rtenv)
|
|
this.rtenv.print = print;
|
|
stdout = print;
|
|
};
|
|
|
|
this.setStderrFunc = function(func) {
|
|
|
|
var printErr = function(text) {
|
|
if (arguments.length > 1)
|
|
text = Array.prototype.slice.call(arguments).join(" ");
|
|
func(text);
|
|
};
|
|
if (this.rtenv)
|
|
this.rtenv.printErr = printErr;
|
|
stderr = printErr;
|
|
};
|
|
|
|
|
|
}; // Engine()
|
|
|
|
Engine.RuntimeEnvironment = engine.RuntimeEnvironment;
|
|
|
|
Engine.initEngine = function(newBasePath) {
|
|
|
|
if (newBasePath !== undefined) basePath = getBasePath(newBasePath);
|
|
if (engineLoadPromise === null) {
|
|
if (USING_WASM) {
|
|
if (typeof WebAssembly !== 'object')
|
|
return Promise.reject(new Error("Browser doesn't support WebAssembly"));
|
|
// TODO cache/retrieve module to/from idb
|
|
engineLoadPromise = loadPromise(basePath + '.wasm').then(function(xhr) {
|
|
return xhr.response;
|
|
});
|
|
} else {
|
|
var asmjsPromise = loadPromise(basePath + '.asm.js').then(function(xhr) {
|
|
return asmjsModulePromise(xhr.response);
|
|
});
|
|
var memPromise = loadPromise(basePath + '.mem');
|
|
engineLoadPromise = Promise.all([asmjsPromise, memPromise]).then(function(values) {
|
|
return { asm: values[0], mem: values[1] };
|
|
});
|
|
}
|
|
engineLoadPromise = engineLoadPromise.catch(function(err) {
|
|
engineLoadPromise = null;
|
|
throw err;
|
|
});
|
|
}
|
|
return engineLoadPromise;
|
|
};
|
|
|
|
function asmjsModulePromise(module) {
|
|
var elem = document.createElement('script');
|
|
var script = new Blob([
|
|
'Engine.asm = (function() { var Module = {};',
|
|
module,
|
|
'return Module.asm; })();'
|
|
]);
|
|
var url = URL.createObjectURL(script);
|
|
elem.src = url;
|
|
return new Promise(function(resolve, reject) {
|
|
elem.addEventListener('load', function() {
|
|
URL.revokeObjectURL(url);
|
|
var asm = Engine.asm;
|
|
Engine.asm = undefined;
|
|
setTimeout(function() {
|
|
// delay to reclaim compilation memory
|
|
resolve(asm);
|
|
}, 1);
|
|
});
|
|
elem.addEventListener('error', function() {
|
|
URL.revokeObjectURL(url);
|
|
reject("asm.js faiilure");
|
|
});
|
|
document.body.appendChild(elem);
|
|
});
|
|
}
|
|
|
|
Engine.unloadEngine = function() {
|
|
engineLoadPromise = null;
|
|
};
|
|
|
|
function loadPromise(file, tracker) {
|
|
if (tracker === undefined)
|
|
tracker = loadingFiles;
|
|
return new Promise(function(resolve, reject) {
|
|
loadXHR(resolve, reject, file, tracker);
|
|
});
|
|
}
|
|
|
|
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', 'timeout', '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 {
|
|
loadXHR(resolve, reject, file);
|
|
}
|
|
}
|
|
|
|
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':
|
|
case 'timeout':
|
|
if (++tracker[file].attempts >= DOWNLOAD_ATTEMPTS_MAX) {
|
|
tracker[file].final = true;
|
|
reject(new Error("Failed loading file '" + file + "'"));
|
|
} else {
|
|
loadXHR(resolve, reject, file);
|
|
}
|
|
break;
|
|
|
|
case 'abort':
|
|
tracker[file].final = true;
|
|
reject(new Error("Loading file '" + file + "' was aborted."));
|
|
break;
|
|
}
|
|
}
|
|
})();
|