Browse Source

[HTML5] Preloader fetch, streaming instantiation.

Fabio Alessandrelli 4 years ago
parent
commit
f64ec5f1ad

+ 13 - 8
platform/javascript/js/engine/config.js

@@ -227,10 +227,10 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
 	/**
 	 * @ignore
 	 * @param {string} loadPath
-	 * @param {Promise} loadPromise
+	 * @param {Response} response
 	 */
-	Config.prototype.getModuleConfig = function (loadPath, loadPromise) {
-		let loader = loadPromise;
+	Config.prototype.getModuleConfig = function (loadPath, response) {
+		let r = response;
 		return {
 			'print': this.onPrint,
 			'printErr': this.onPrintError,
@@ -238,12 +238,17 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
 			'noExitRuntime': true,
 			'dynamicLibraries': [`${loadPath}.side.wasm`],
 			'instantiateWasm': function (imports, onSuccess) {
-				loader.then(function (xhr) {
-					WebAssembly.instantiate(xhr.response, imports).then(function (result) {
-						onSuccess(result['instance'], result['module']);
+				function done(result) {
+					onSuccess(result['instance'], result['module']);
+				}
+				if (typeof (WebAssembly.instantiateStreaming) !== 'undefined') {
+					WebAssembly.instantiateStreaming(Promise.resolve(r), imports).then(done);
+				} else {
+					r.arrayBuffer().then(function (buffer) {
+						WebAssembly.instantiate(buffer, imports).then(done);
 					});
-				});
-				loader = null;
+				}
+				r = null;
 				return {};
 			},
 			'locateFile': function (path) {

+ 1 - 0
platform/javascript/js/engine/engine.externs.js

@@ -1,3 +1,4 @@
 var Godot;
 var WebAssembly = {};
 WebAssembly.instantiate = function(buffer, imports) {};
+WebAssembly.instantiateStreaming = function(response, imports) {};

+ 17 - 13
platform/javascript/js/engine/engine.js

@@ -42,7 +42,7 @@ const Engine = (function () {
 	Engine.load = function (basePath) {
 		if (loadPromise == null) {
 			loadPath = basePath;
-			loadPromise = preloader.loadPromise(`${loadPath}.wasm`);
+			loadPromise = preloader.loadPromise(`${loadPath}.wasm`, true);
 			requestAnimationFrame(preloader.animateProgress);
 		}
 		return loadPromise;
@@ -98,21 +98,25 @@ const Engine = (function () {
 					}
 					Engine.load(basePath);
 				}
-				preloader.setProgressFunc(this.config.onProgress);
-				let config = this.config.getModuleConfig(loadPath, loadPromise);
 				const me = this;
-				initPromise = new Promise(function (resolve, reject) {
-					Godot(config).then(function (module) {
-						module['initFS'](me.config.persistentPaths).then(function (fs_err) {
-							me.rtenv = module;
-							if (me.config.unloadAfterInit) {
-								Engine.unload();
-							}
-							resolve();
-							config = null;
+				function doInit(promise) {
+					return promise.then(function (response) {
+						return Godot(me.config.getModuleConfig(loadPath, response.clone()));
+					}).then(function (module) {
+						const paths = me.config.persistentPaths;
+						return module['initFS'](paths).then(function (err) {
+							return Promise.resolve(module);
 						});
+					}).then(function (module) {
+						me.rtenv = module;
+						if (me.config.unloadAfterInit) {
+							Engine.unload();
+						}
+						return Promise.resolve();
 					});
-				});
+				}
+				preloader.setProgressFunc(this.config.onProgress);
+				initPromise = doInit(loadPromise);
 				return initPromise;
 			},
 

+ 72 - 49
platform/javascript/js/engine/preloader.js

@@ -1,54 +1,79 @@
 const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars
-	const loadXHR = function (resolve, reject, file, tracker, attempts) {
-		const xhr = new XMLHttpRequest();
+	function getTrackedResponse(response, load_status) {
+		let clen = 0;
+		let compressed = false;
+		response.headers.forEach(function (value, header) {
+			const h = header.toLowerCase().trim();
+			// We can't accurately compute compressed stream length.
+			if (h === 'content-encoding') {
+				compressed = true;
+			} else if (h === 'content-length') {
+				const length = parseInt(value, 10);
+				if (!Number.isNaN(length) && length > 0) {
+					clen = length;
+				}
+			}
+		});
+		if (!compressed && clen) {
+			load_status.total = clen;
+		}
+		function onloadprogress(reader, controller) {
+			return reader.read().then(function (result) {
+				if (load_status.done) {
+					return Promise.resolve();
+				}
+				if (result.value) {
+					controller.enqueue(result.value);
+					load_status.loaded += result.value.length;
+				}
+				if (!result.done) {
+					return onloadprogress(reader, controller);
+				}
+				load_status.done = true;
+				return Promise.resolve();
+			});
+		}
+		const reader = response.body.getReader();
+		return new Response(new ReadableStream({
+			start: function (controller) {
+				onloadprogress(reader, controller).then(function () {
+					controller.close();
+				});
+			},
+		}), { headers: response.headers });
+	}
+
+	function loadFetch(file, tracker, raw) {
 		tracker[file] = {
 			total: 0,
 			loaded: 0,
-			final: false,
+			done: false,
 		};
-		xhr.onerror = function () {
+		return fetch(file).then(function (response) {
+			if (!response.ok) {
+				return Promise.reject(new Error(`Failed loading file '${file}'`));
+			}
+			const tr = getTrackedResponse(response, tracker[file]);
+			if (raw) {
+				return Promise.resolve(tr);
+			}
+			return tr.arrayBuffer();
+		});
+	}
+
+	function retry(func, attempts = 1) {
+		function onerror(err) {
 			if (attempts <= 1) {
-				reject(new Error(`Failed loading file '${file}'`));
-			} else {
+				return Promise.reject(err);
+			}
+			return new Promise(function (resolve, reject) {
 				setTimeout(function () {
-					loadXHR(resolve, reject, file, tracker, attempts - 1);
+					retry(func, attempts - 1).then(resolve).catch(reject);
 				}, 1000);
-			}
-		};
-		xhr.onabort = function () {
-			tracker[file].final = true;
-			reject(new Error(`Loading file '${file}' was aborted.`));
-		};
-		xhr.onloadstart = function (ev) {
-			tracker[file].total = ev.total;
-			tracker[file].loaded = ev.loaded;
-		};
-		xhr.onprogress = function (ev) {
-			tracker[file].loaded = ev.loaded;
-			tracker[file].total = ev.total;
-		};
-		xhr.onload = function () {
-			if (xhr.status >= 400) {
-				if (xhr.status < 500 || attempts <= 1) {
-					reject(new Error(`Failed loading file '${file}': ${xhr.statusText}`));
-					xhr.abort();
-				} else {
-					setTimeout(function () {
-						loadXHR(resolve, reject, file, tracker, attempts - 1);
-					}, 1000);
-				}
-			} else {
-				tracker[file].final = true;
-				resolve(xhr);
-			}
-		};
-		// Make request.
-		xhr.open('GET', file);
-		if (!file.endsWith('.js')) {
-			xhr.responseType = 'arraybuffer';
+			});
 		}
-		xhr.send();
-	};
+		return func().catch(onerror);
+	}
 
 	const DOWNLOAD_ATTEMPTS_MAX = 4;
 	const loadingFiles = {};
@@ -63,7 +88,7 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un
 
 		Object.keys(loadingFiles).forEach(function (file) {
 			const stat = loadingFiles[file];
-			if (!stat.final) {
+			if (!stat.done) {
 				progressIsFinal = false;
 			}
 			if (!totalIsValid || stat.total === 0) {
@@ -92,10 +117,8 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un
 		progressFunc = callback;
 	};
 
-	this.loadPromise = function (file) {
-		return new Promise(function (resolve, reject) {
-			loadXHR(resolve, reject, file, loadingFiles, DOWNLOAD_ATTEMPTS_MAX);
-		});
+	this.loadPromise = function (file, raw = false) {
+		return retry(loadFetch.bind(null, file, loadingFiles, raw), DOWNLOAD_ATTEMPTS_MAX);
 	};
 
 	this.preloadedFiles = [];
@@ -103,10 +126,10 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un
 		let buffer = null;
 		if (typeof pathOrBuffer === 'string') {
 			const me = this;
-			return this.loadPromise(pathOrBuffer).then(function (xhr) {
+			return this.loadPromise(pathOrBuffer).then(function (buf) {
 				me.preloadedFiles.push({
 					path: destPath || pathOrBuffer,
-					buffer: xhr.response,
+					buffer: buf,
 				});
 				return Promise.resolve();
 			});