[HTML5] Preloader fetch, streaming instantiation.

This commit is contained in:
Fabio Alessandrelli 2021-03-01 11:26:22 +01:00
parent 8a020a6573
commit f64ec5f1ad
4 changed files with 103 additions and 70 deletions

View File

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

View File

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

View File

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

View File

@ -1,54 +1,79 @@
const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars
const loadXHR = function (resolve, reject, file, tracker, attempts) { function getTrackedResponse(response, load_status) {
const xhr = new XMLHttpRequest(); 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] = { tracker[file] = {
total: 0, total: 0,
loaded: 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) { if (attempts <= 1) {
reject(new Error(`Failed loading file '${file}'`)); return Promise.reject(err);
} else { }
return new Promise(function (resolve, reject) {
setTimeout(function () { setTimeout(function () {
loadXHR(resolve, reject, file, tracker, attempts - 1); retry(func, attempts - 1).then(resolve).catch(reject);
}, 1000); }, 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 DOWNLOAD_ATTEMPTS_MAX = 4;
const loadingFiles = {}; const loadingFiles = {};
@ -63,7 +88,7 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un
Object.keys(loadingFiles).forEach(function (file) { Object.keys(loadingFiles).forEach(function (file) {
const stat = loadingFiles[file]; const stat = loadingFiles[file];
if (!stat.final) { if (!stat.done) {
progressIsFinal = false; progressIsFinal = false;
} }
if (!totalIsValid || stat.total === 0) { if (!totalIsValid || stat.total === 0) {
@ -92,10 +117,8 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un
progressFunc = callback; progressFunc = callback;
}; };
this.loadPromise = function (file) { this.loadPromise = function (file, raw = false) {
return new Promise(function (resolve, reject) { return retry(loadFetch.bind(null, file, loadingFiles, raw), DOWNLOAD_ATTEMPTS_MAX);
loadXHR(resolve, reject, file, loadingFiles, DOWNLOAD_ATTEMPTS_MAX);
});
}; };
this.preloadedFiles = []; this.preloadedFiles = [];
@ -103,10 +126,10 @@ const Preloader = /** @constructor */ function () { // eslint-disable-line no-un
let buffer = null; let buffer = null;
if (typeof pathOrBuffer === 'string') { if (typeof pathOrBuffer === 'string') {
const me = this; const me = this;
return this.loadPromise(pathOrBuffer).then(function (xhr) { return this.loadPromise(pathOrBuffer).then(function (buf) {
me.preloadedFiles.push({ me.preloadedFiles.push({
path: destPath || pathOrBuffer, path: destPath || pathOrBuffer,
buffer: xhr.response, buffer: buf,
}); });
return Promise.resolve(); return Promise.resolve();
}); });