Răsfoiți Sursa

Merge pull request #11154 from eska014/html5-startup-revamp

HTML5 start-up overhaul with download progress display
Poommetee Ketson 8 ani în urmă
părinte
comite
8bf8cf1316

+ 386 - 0
misc/dist/html/default.html

@@ -0,0 +1,386 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
+<head>
+	<meta charset="utf-8" />
+	<title></title>
+	<style type="text/css">
+
+		body {
+			margin: 0;
+			border: 0 none;
+			padding: 0;
+			text-align: center;
+			background-color: #222226;
+			font-family: 'Noto Sans', Arial, sans-serif;
+		}
+
+
+		/* Godot Engine default theme style
+		 * ================================ */
+
+		.godot {
+			color: #e0e0e0;
+			background-color: #3b3943;
+			background-image: linear-gradient(to bottom, #403e48, #35333c);
+			border: 1px solid #45434e;
+			box-shadow: 0 0 1px 1px #2f2d35;
+		}
+
+		button.godot {
+			font-family: 'Droid Sans', Arial, sans-serif; /* override user agent style */
+			padding: 1px 5px;
+			background-color: #37353f;
+			background-image: linear-gradient(to bottom, #413e49, #3a3842);
+			border: 1px solid #514f5d;
+			border-radius: 1px;
+			box-shadow: 0 0 1px 1px #2a2930;
+		}
+
+		button.godot:hover {
+			color: #f0f0f0;
+			background-color: #44414e;
+			background-image: linear-gradient(to bottom, #494652, #423f4c);
+			border: 1px solid #5a5667;
+			box-shadow: 0 0 1px 1px #26252b;
+		}
+
+		button.godot:active {
+			color: #fff;
+			background-color: #3e3b46;
+			background-image: linear-gradient(to bottom, #36343d, #413e49);
+			border: 1px solid #4f4c59;
+			box-shadow: 0 0 1px 1px #26252b;
+		}
+
+		button.godot:disabled {
+			color: rgba(230, 230, 230, 0.2);
+			background-color: #3d3d3d;
+			background-image: linear-gradient(to bottom, #434343, #393939);
+			border: 1px solid #474747;
+			box-shadow: 0 0 1px 1px #2d2b33;
+		}
+
+
+		/* Canvas / wrapper
+		 * ================ */
+
+		#container {
+			display: inline-block; /* scale with canvas */
+			vertical-align: top; /* prevent extra height */
+			position: relative; /* root for absolutely positioned overlay */
+			margin: 0;
+			border: 0 none;
+			padding: 0;
+			background-color: #0c0c0c;
+		}
+
+		#canvas {
+			display: block;
+			margin: 0 auto;
+			color: white;
+		}
+
+		#canvas:focus {
+			outline: none;
+		}
+
+
+		/* Status display
+		 * ============== */
+
+		#status {
+			position: absolute;
+			left: 0;
+			top: 0;
+			right: 0;
+			bottom: 0;
+			display: flex;
+			justify-content: center;
+			align-items: center;
+			/* don't consume click events - make children visible explicitly */
+			visibility: hidden;
+		}
+
+		#status-progress {
+			width: 244px;
+			height: 7px;
+			background-color: #38363A;
+			border: 1px solid #444246;
+			padding: 1px;
+			box-shadow: 0 0 2px 1px #1B1C22;
+			border-radius: 2px;
+			visibility: visible;
+		}
+
+		#status-progress-inner {
+			height: 100%;
+			width: 0;
+			box-sizing: border-box;
+			transition: width 0.5s linear;
+			background-color: #202020;
+			border: 1px solid #222223;
+			box-shadow: 0 0 1px 1px #27282E;
+			border-radius: 3px;
+		}
+
+		#status-indeterminate {
+			visibility: visible;
+			position: relative;
+		}
+
+		#status-indeterminate > div {
+			width: 3px;
+			height: 0;
+			border-style: solid;
+			border-width: 6px 2px 0 2px;
+			border-color: #2b2b2b transparent transparent transparent;
+			transform-origin: center 14px;
+			position: absolute;
+		}
+
+		#status-indeterminate > div:nth-child(1) { transform: rotate( 22.5deg); }
+		#status-indeterminate > div:nth-child(2) { transform: rotate( 67.5deg); }
+		#status-indeterminate > div:nth-child(3) { transform: rotate(112.5deg); }
+		#status-indeterminate > div:nth-child(4) { transform: rotate(157.5deg); }
+		#status-indeterminate > div:nth-child(5) { transform: rotate(202.5deg); }
+		#status-indeterminate > div:nth-child(6) { transform: rotate(247.5deg); }
+		#status-indeterminate > div:nth-child(7) { transform: rotate(292.5deg); }
+		#status-indeterminate > div:nth-child(8) { transform: rotate(337.5deg); }
+
+		#status-notice {
+			margin: 0 100px;
+			line-height: 1.3;
+			visibility: visible;
+			padding: 4px 6px;
+			visibility: visible;
+		}
+
+
+		/* Debug output
+		 * ============ */
+
+		#output-panel {
+			display: none;
+			max-width: 700px;
+			font-size: small;
+			margin: 6px auto 0;
+			padding: 0 4px 4px;
+			text-align: left;
+			line-height: 2.2;
+		}
+
+		#output-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+		}
+
+		#output-container {
+			padding: 6px;
+			background-color: #2c2a32;
+			box-shadow: inset 0 0 1px 1px #232127;
+			color: #bbb;
+		}
+
+		#output-scroll {
+			line-height: 1;
+			height: 12em;
+			overflow-y: scroll;
+			white-space: pre-wrap;
+			font-size: small;
+			font-family: "Lucida Console", Monaco, monospace;
+		}
+	</style>
+$GODOT_HEAD_INCLUDE
+</head>
+<body>
+	<div id="container">
+		<canvas id="canvas" oncontextmenu="event.preventDefault();" width="640" height="480">
+			HTML5 canvas appears to be unsupported in the current browser.<br />
+			Please try updating or use a different browser.
+		</canvas>
+		<div id="status">
+			<div id='status-progress' style='display: none;' oncontextmenu="event.preventDefault();"><div id ='status-progress-inner'></div></div>
+			<div id='status-indeterminate' style='display: none;' oncontextmenu="event.preventDefault();">
+				<div></div>
+				<div></div>
+				<div></div>
+				<div></div>
+				<div></div>
+				<div></div>
+				<div></div>
+				<div></div>
+			</div>
+			<div id="status-notice" class="godot" style='display: none;'></div>
+		</div>
+	</div>
+	<div id="output-panel" class="godot">
+		<div id="output-header">
+			Output:
+			<button id='output-clear' class='godot' type='button' autocomplete='off'>Clear</button>
+		</div>
+		<div id="output-container"><div id="output-scroll"></div></div>
+	</div>
+
+	<script type="text/javascript" src="$GODOT_BASENAME.js"></script>
+	<script type="text/javascript">//<![CDATA[
+
+		var game = new Engine;
+
+		(function() {
+
+			const BASENAME = '$GODOT_BASENAME';
+			const MEMORY_SIZE = $GODOT_TOTAL_MEMORY;
+			const DEBUG_ENABLED = $GODOT_DEBUG_ENABLED;
+			const INDETERMINATE_STATUS_STEP_MS = 100;
+
+			var container = document.getElementById('container');
+			var canvas = document.getElementById('canvas');
+			var statusProgress = document.getElementById('status-progress');
+			var statusProgressInner = document.getElementById('status-progress-inner');
+			var statusIndeterminate = document.getElementById('status-indeterminate');
+			var statusNotice = document.getElementById('status-notice');
+
+			var initializing = true;
+			var statusMode = 'hidden';
+			var indeterminiateStatusAnimationId = 0;
+
+			setStatusMode('indeterminate');
+			game.setCanvas(canvas);
+			game.setAsmjsMemorySize(MEMORY_SIZE);
+
+			function setStatusMode(mode) {
+
+				if (statusMode === mode || !initializing)
+					return;
+				[statusProgress, statusIndeterminate, statusNotice].forEach(elem => {
+					elem.style.display = 'none';
+				});
+				if (indeterminiateStatusAnimationId !== 0) {
+					cancelAnimationFrame(indeterminiateStatusAnimationId);
+					indeterminiateStatusAnimationId = 0;
+				}
+				switch (mode) {
+					case 'progress':
+						statusProgress.style.display = 'block';
+						break;
+					case 'indeterminate':
+						statusIndeterminate.style.display = 'block';
+						indeterminiateStatusAnimationId = requestAnimationFrame(animateStatusIndeterminate);
+						break;
+					case 'notice':
+						statusNotice.style.display = 'block';
+						break;
+					case 'hidden':
+						break;
+					default:
+						throw new Error("Invalid status mode");
+				}
+				statusMode = mode;
+			}
+
+			function animateStatusIndeterminate(ms) {
+				var i = Math.floor(ms / INDETERMINATE_STATUS_STEP_MS % 8);
+				if (statusIndeterminate.children[i].style.borderTopColor == '') {
+					Array.prototype.slice.call(statusIndeterminate.children).forEach(child => {
+						child.style.borderTopColor = '';
+					});
+					statusIndeterminate.children[i].style.borderTopColor = '#dfdfdf';
+				}
+				requestAnimationFrame(animateStatusIndeterminate);
+			}
+
+			function setStatusNotice(text) {
+
+				while (statusNotice.lastChild) {
+					statusNotice.removeChild(statusNotice.lastChild);
+				}
+				var lines = text.split('\n');
+				lines.forEach((line, index) => {
+					statusNotice.appendChild(document.createTextNode(line));
+					statusNotice.appendChild(document.createElement('br'));
+				});
+			};
+
+			game.setProgressFunc((current, total) => {
+
+				if (total > 0) {
+					statusProgressInner.style.width = current/total * 100 + '%';
+					setStatusMode('progress');
+					if (current === total) {
+						// wait for progress bar animation
+						setTimeout(() => {
+							setStatusMode('indeterminate');
+						}, 500);
+					}
+				} else {
+					setStatusMode('indeterminate');
+				}
+			});
+
+			if (DEBUG_ENABLED) {
+				var outputRoot = document.getElementById("output-panel");
+				var outputScroll = document.getElementById("output-scroll");
+				var OUTPUT_MSG_COUNT_MAX = 400;
+
+				document.getElementById('output-clear').addEventListener('click', () => {
+					while (outputScroll.firstChild) {
+						outputScroll.firstChild.remove();
+					}
+				});
+
+				outputRoot.style.display = 'block';
+
+				function print(text) {
+					if (arguments.length > 1) {
+						text = Array.prototype.slice.call(arguments).join(" ");
+					}
+					if (text.length <= 0) return;
+					while (outputScroll.childElementCount >= OUTPUT_MSG_COUNT_MAX) {
+						outputScroll.firstChild.remove();
+					}
+					var msg = document.createElement("div");
+					if (String.prototype.trim.call(text).startsWith("**ERROR**")) {
+						msg.style.color = "#d44";
+					} else if (String.prototype.trim.call(text).startsWith("**WARNING**")) {
+						msg.style.color = "#ccc000";
+					} else if (String.prototype.trim.call(text).startsWith("**SCRIPT ERROR**")) {
+						msg.style.color = "#c6d";
+					}
+					msg.textContent = text;
+					var scrollToBottom = outputScroll.scrollHeight - (outputScroll.clientHeight + outputScroll.scrollTop) < 10;
+					outputScroll.appendChild(msg);
+					if (scrollToBottom) {
+						outputScroll.scrollTop = outputScroll.scrollHeight;
+					}
+				};
+
+				function printError(text) {
+					print('**ERROR**' + ":", text);
+				}
+
+				game.setStdoutFunc(text => {
+					print(text);
+					console.log(text);
+				});
+
+				game.setStderrFunc(text => {
+					printError(text);
+					console.warn(text);
+				});
+			}
+
+			game.start(BASENAME + '.pck').then(() => {
+				setStatusMode('hidden');
+				initializing = false;
+			}, err => {
+				if (DEBUG_ENABLED)
+					printError(err.message);
+				setStatusNotice(err.message);
+				setStatusMode('notice');
+				initializing = false;
+			});
+		})();
+	//]]></script>
+</body>
+</html>

+ 0 - 151
misc/dist/html_fs/godotfs.js

@@ -1,151 +0,0 @@
-
-var Module;
-if (typeof Module === 'undefined') Module = eval('(function() { try { return Module || {} } catch(e) { return {} } })()');
-if (!Module.expectedDataFileDownloads) {
-  Module.expectedDataFileDownloads = 0;
-  Module.finishedDataFileDownloads = 0;
-}
-Module.expectedDataFileDownloads++;
-(function() {
-
-    const PACK_FILE_NAME = '$GODOT_PACK_NAME';
-    const PACK_FILE_SIZE = $GODOT_PACK_SIZE;
-    function fetchRemotePackage(packageName, callback, errback) {
-      var xhr = new XMLHttpRequest();
-      xhr.open('GET', packageName, true);
-      xhr.responseType = 'arraybuffer';
-      xhr.onprogress = function(event) {
-        var url = packageName;
-        if (event.loaded && event.total) {
-          if (!xhr.addedTotal) {
-            xhr.addedTotal = true;
-            if (!Module.dataFileDownloads) Module.dataFileDownloads = {};
-            Module.dataFileDownloads[url] = {
-              loaded: event.loaded,
-              total: event.total
-            };
-          } else {
-            Module.dataFileDownloads[url].loaded = event.loaded;
-          }
-          var total = 0;
-          var loaded = 0;
-          var num = 0;
-          for (var download in Module.dataFileDownloads) {
-          var data = Module.dataFileDownloads[download];
-            total += data.total;
-            loaded += data.loaded;
-            num++;
-          }
-          total = Math.ceil(total * Module.expectedDataFileDownloads/num);
-          if (Module['setStatus']) Module['setStatus']('Downloading data... (' + loaded + '/' + total + ')');
-        } else if (!Module.dataFileDownloads) {
-          if (Module['setStatus']) Module['setStatus']('Downloading data...');
-        }
-      };
-      xhr.onload = function(event) {
-        var packageData = xhr.response;
-        callback(packageData);
-      };
-      xhr.send(null);
-    };
-
-    function handleError(error) {
-      console.error('package error:', error);
-    };
-
-      var fetched = null, fetchedCallback = null;
-      fetchRemotePackage(PACK_FILE_NAME, function(data) {
-        if (fetchedCallback) {
-          fetchedCallback(data);
-          fetchedCallback = null;
-        } else {
-          fetched = data;
-        }
-      }, handleError);
-
-  function runWithFS() {
-
-function assert(check, msg) {
-  if (!check) throw msg + new Error().stack;
-}
-
-    function DataRequest(start, end, crunched, audio) {
-      this.start = start;
-      this.end = end;
-      this.crunched = crunched;
-      this.audio = audio;
-    }
-    DataRequest.prototype = {
-      requests: {},
-      open: function(mode, name) {
-        this.name = name;
-        this.requests[name] = this;
-        Module['addRunDependency']('fp ' + this.name);
-      },
-      send: function() {},
-      onload: function() {
-        var byteArray = this.byteArray.subarray(this.start, this.end);
-
-          this.finish(byteArray);
-
-      },
-      finish: function(byteArray) {
-        var that = this;
-        Module['FS_createPreloadedFile'](this.name, null, byteArray, true, true, function() {
-          Module['removeRunDependency']('fp ' + that.name);
-        }, function() {
-          if (that.audio) {
-            Module['removeRunDependency']('fp ' + that.name); // workaround for chromium bug 124926 (still no audio with this, but at least we don't hang)
-          } else {
-            Module.printErr('Preloading file ' + that.name + ' failed');
-          }
-        }, false, true); // canOwn this data in the filesystem, it is a slide into the heap that will never change
-        this.requests[this.name] = null;
-      },
-    };
-      new DataRequest(0, PACK_FILE_SIZE, 0, 0).open('GET', '/' + PACK_FILE_NAME);
-
-    var PACKAGE_PATH;
-    if (typeof window === 'object') {
-      PACKAGE_PATH = window['encodeURIComponent'](window.location.pathname.toString().substring(0, window.location.pathname.toString().lastIndexOf('/')) + '/');
-    } else {
-      // worker
-      PACKAGE_PATH = encodeURIComponent(location.pathname.toString().substring(0, location.pathname.toString().lastIndexOf('/')) + '/');
-    }
-    var PACKAGE_NAME = PACK_FILE_NAME;
-    var REMOTE_PACKAGE_NAME = PACK_FILE_NAME;
-    var PACKAGE_UUID = 'b39761ce-0348-4959-9b16-302ed8e1592e';
-
-    function processPackageData(arrayBuffer) {
-      Module.finishedDataFileDownloads++;
-      assert(arrayBuffer, 'Loading data file failed.');
-      var byteArray = new Uint8Array(arrayBuffer);
-      var curr;
-
-      // Reuse the bytearray from the XHR as the source for file reads.
-      DataRequest.prototype.byteArray = byteArray;
-          DataRequest.prototype.requests['/' + PACK_FILE_NAME].onload();
-          Module['removeRunDependency']('datafile_datapack');
-
-    };
-    Module['addRunDependency']('datafile_datapack');
-
-    if (!Module.preloadResults) Module.preloadResults = {};
-
-      Module.preloadResults[PACKAGE_NAME] = {fromCache: false};
-      if (fetched) {
-        processPackageData(fetched);
-        fetched = null;
-      } else {
-        fetchedCallback = processPackageData;
-      }
-
-  }
-  if (Module['calledRun']) {
-    runWithFS();
-  } else {
-    if (!Module['preRun']) Module['preRun'] = [];
-    Module["preRun"].push(runWithFS); // FS is not initialized yet, wait for it
-  }
-
-})();

+ 16 - 17
platform/javascript/SCsub

@@ -20,33 +20,32 @@ for x in javascript_files:
     javascript_objects.append(env_javascript.Object(x))
 
 env.Append(LINKFLAGS=["-s", "EXPORTED_FUNCTIONS=\"['_main','_audio_server_mix_function','_main_after_fs_sync','_send_notification']\""])
-env.Append(LINKFLAGS=["--shell-file", '"platform/javascript/godot_shell.html"'])
 
 # output file name without file extension
 basename = "godot" + env["PROGSUFFIX"]
 target_dir = env.Dir("#bin")
-js_file = target_dir.File(basename + ".js")
-implicit_targets = [js_file]
 
 zip_dir = target_dir.Dir('.javascript_zip')
-zip_files = env.InstallAs([zip_dir.File("godot.js"), zip_dir.File("godotfs.js")], [js_file, "#misc/dist/html_fs/godotfs.js"])
+zip_files = env.InstallAs(zip_dir.File('godot.html'), '#misc/dist/html/default.html')
 
+implicit_targets = []
 if env['wasm'] == 'yes':
-    wasm_file = target_dir.File(basename+'.wasm')
-    implicit_targets.append(wasm_file)
-    zip_files.append(InstallAs(zip_dir.File('godot.wasm'), wasm_file))
+    wasm = target_dir.File(basename + '.wasm')
+    implicit_targets.append(wasm)
+    zip_files.append(InstallAs(zip_dir.File('godot.wasm'), wasm))
+    prejs = env.File('pre_wasm.js')
 else:
-    asmjs_files = [target_dir.File(basename+'.asm.js'), target_dir.File(basename+'.html.mem')]
-    zip_files.append(InstallAs([zip_dir.File('godot.asm.js'), zip_dir.File('godot.mem')], asmjs_files))
+    asmjs_files = [target_dir.File(basename + '.asm.js'), target_dir.File(basename + '.js.mem')]
     implicit_targets.extend(asmjs_files)
+    zip_files.append(InstallAs([zip_dir.File('godot.asm.js'), zip_dir.File('godot.mem')], asmjs_files))
+    prejs = env.File('pre_asmjs.js')
 
-# HTML file must be the first target in the list
-html_file = env.Program(["#bin/godot"] + implicit_targets, javascript_objects, PROGSUFFIX=env["PROGSUFFIX"]+".html")[0]
-Depends(html_file, "godot_shell.html")
+js = env.Program(['#bin/godot'] + implicit_targets, javascript_objects, PROGSUFFIX=env['PROGSUFFIX'] + '.js')[0];
+zip_files.append(InstallAs(zip_dir.File('godot.js'), js))
 
-# Emscripten hardcodes file names, so replace common base name with
-# placeholder while leaving extension; also change `.html.mem` to just `.mem`
-fixup_html = env.Substfile(html_file, SUBST_DICT=[(basename, '$$GODOT_BASE'), ('.html.mem', '.mem')], SUBSTFILESUFFIX='.fixup.html')
+postjs = env.File('engine.js')
+env.Depends(js, [prejs, postjs])
+env.Append(LINKFLAGS=['--pre-js', prejs.path])
+env.Append(LINKFLAGS=['--post-js', postjs.path])
 
-zip_files.append(InstallAs(zip_dir.File('godot.html'), fixup_html))
-Zip('#bin/godot', zip_files, ZIPSUFFIX=env['PROGSUFFIX']+env['ZIPSUFFIX'], ZIPROOT=zip_dir, ZIPCOMSTR="Archving $SOURCES as $TARGET")
+Zip('#bin/godot', zip_files, ZIPSUFFIX=env['PROGSUFFIX'] + env['ZIPSUFFIX'], ZIPROOT=zip_dir, ZIPCOMSTR="Archving $SOURCES as $TARGET")

+ 2 - 0
platform/javascript/detect.py

@@ -100,6 +100,7 @@ def configure(env):
 
     ## Link flags
 
+    env.Append(LINKFLAGS=['-s', 'EXTRA_EXPORTED_RUNTIME_METHODS="[\'FS\']"'])
     env.Append(LINKFLAGS=['-s', 'USE_WEBGL2=1'])
 
     if (env['wasm'] == 'yes'):
@@ -112,6 +113,7 @@ def configure(env):
     else:
         env.Append(LINKFLAGS=['-s', 'ASM_JS=1'])
         env.Append(LINKFLAGS=['--separate-asm'])
+        env.Append(LINKFLAGS=['--memory-init-file', '1'])
 
     # TODO: Move that to opus module's config
     if("module_opus_enabled" in env and env["module_opus_enabled"] != "no"):

+ 366 - 0
platform/javascript/engine.js

@@ -0,0 +1,366 @@
+		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 WebAssembly.Module) {
+				rtenvOpts.instantiateWasm = function(imports, onSuccess) {
+					WebAssembly.instantiate(initializer, imports).then(function(result) {
+						onSuccess(result);
+					});
+					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 WebAssembly.compile(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;
+		}
+	}
+})();

+ 2 - 28
platform/javascript/export/export.cpp

@@ -100,8 +100,8 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re
 	for (int i = 0; i < lines.size(); i++) {
 
 		String current_line = lines[i];
-		current_line = current_line.replace("$GODOT_TMEM", itos(memory_mb * 1024 * 1024));
-		current_line = current_line.replace("$GODOT_BASE", p_name);
+		current_line = current_line.replace("$GODOT_TOTAL_MEMORY", itos(memory_mb * 1024 * 1024));
+		current_line = current_line.replace("$GODOT_BASENAME", p_name);
 		current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include"));
 		current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false");
 		str_export += current_line + "\n";
@@ -114,28 +114,6 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re
 	}
 }
 
-void EditorExportPlatformJavaScript::_fix_fsloader_js(Vector<uint8_t> &p_js, const String &p_pack_name, uint64_t p_pack_size) {
-
-	String str_template = String::utf8(reinterpret_cast<const char *>(p_js.ptr()), p_js.size());
-	String str_export;
-	Vector<String> lines = str_template.split("\n");
-	for (int i = 0; i < lines.size(); i++) {
-		if (lines[i].find("$GODOT_PACK_NAME") != -1) {
-			str_export += lines[i].replace("$GODOT_PACK_NAME", p_pack_name);
-		} else if (lines[i].find("$GODOT_PACK_SIZE") != -1) {
-			str_export += lines[i].replace("$GODOT_PACK_SIZE", itos(p_pack_size));
-		} else {
-			str_export += lines[i] + "\n";
-		}
-	}
-
-	CharString cs = str_export.utf8();
-	p_js.resize(cs.length());
-	for (int i = 0; i < cs.length(); i++) {
-		p_js[i] = cs[i];
-	}
-}
-
 void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) {
 
 	if (p_preset->get("texture_format/s3tc")) {
@@ -286,10 +264,6 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 
 			_fix_html(data, p_preset, p_path.get_file().get_basename(), p_debug);
 			file = p_path.get_file();
-		} else if (file == "godotfs.js") {
-
-			_fix_fsloader_js(data, pck_path.get_file(), pack_size);
-			file = p_path.get_file().get_basename() + "fs.js";
 		} else if (file == "godot.js") {
 
 			file = p_path.get_file().get_basename() + ".js";

+ 0 - 347
platform/javascript/godot_shell.html

@@ -1,347 +0,0 @@
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
-<head>
-	<meta charset="utf-8" />
-	<title></title>
-	<style type="text/css">
-		body {
-			margin: 0;
-			border: 0 none;
-			padding: 0;
-			text-align: center;
-			background-color: #222226;
-			font-family: 'Droid Sans', Arial, sans-serif;
-		}
-
-
-		/* Godot Engine default theme style
-		 * ================================ */
-
-		.godot {
-			color: #e0e0e0;
-			background-color: #3b3943;
-			background-image: linear-gradient(to bottom, #403e48, #35333c);
-			border: 1px solid #45434e;
-			box-shadow: 0 0 1px 1px #2f2d35;
-		}
-
-		button.godot {
-			font-family: 'Droid Sans', Arial, sans-serif; /* override user agent style */
-			padding: 1px 5px;
-			background-color: #37353f;
-			background-image: linear-gradient(to bottom, #413e49, #3a3842);
-			border: 1px solid #514f5d;
-			border-radius: 1px;
-			box-shadow: 0 0 1px 1px #2a2930;
-		}
-
-		button.godot:hover {
-			color: #f0f0f0;
-			background-color: #44414e;
-			background-image: linear-gradient(to bottom, #494652, #423f4c);
-			border: 1px solid #5a5667;
-			box-shadow: 0 0 1px 1px #26252b;
-		}
-
-		button.godot:active {
-			color: #fff;
-			background-color: #3e3b46;
-			background-image: linear-gradient(to bottom, #36343d, #413e49);
-			border: 1px solid #4f4c59;
-			box-shadow: 0 0 1px 1px #26252b;
-		}
-
-		button.godot:disabled {
-			color: rgba(230, 230, 230, 0.2);
-			background-color: #3d3d3d;
-			background-image: linear-gradient(to bottom, #434343, #393939);
-			border: 1px solid #474747;
-			box-shadow: 0 0 1px 1px #2d2b33;
-		}
-
-
-		/* Canvas / wrapper
-		 * ================ */
-
-		#container {
-			display: inline-block; /* scale with canvas */
-			vertical-align: top; /* prevent extra height */
-			position: relative; /* root for absolutely positioned overlay */
-			margin: 0;
-			border: 0 none;
-			padding: 0;
-			background-color: #0c0c0c;
-		}
-
-		#canvas {
-			display: block;
-			margin: 0 auto;
-			/* canvas must have border and padding set to zero to
-			 * calculate cursor coordinates correctly */
-			border: 0 none;
-			padding: 0;
-			color: white;
-		}
-
-		#canvas:focus {
-			outline: none;
-		}
-
-
-		/* Status display
-		 * ============== */
-
-		#status-container {
-			position: absolute;
-			left: 0;
-			top: 0;
-			right: 0;
-			bottom: 0;
-			display: flex;
-			justify-content: center;
-			align-items: center;
-			/* don't consume click events - make children visible explicitly */
-			visibility: hidden;
-		}
-
-		#status {
-			line-height: 1.3;
-			cursor: pointer;
-			visibility: visible;
-			padding: 4px 6px;
-		}
-
-
-		/* Debug output
-		 * ============ */
-
-		#output-panel {
-			display: none;
-			max-width: 700px;
-			font-size: small;
-			margin: 6px auto 0;
-			padding: 0 4px 4px;
-			text-align: left;
-			line-height: 2.2;
-		}
-
-		#output-header {
-			display: flex;
-			justify-content: space-between;
-			align-items: center;
-		}
-
-		#output-container {
-			padding: 6px;
-			background-color: #2c2a32;
-			box-shadow: inset 0 0 1px 1px #232127;
-			color: #bbb;
-		}
-
-		#output-scroll {
-			line-height: 1;
-			height: 12em;
-			overflow-y: scroll;
-			white-space: pre-wrap;
-			font-size: small;
-			font-family: "Lucida Console", Monaco, monospace;
-		}
-	</style>
-$GODOT_HEAD_INCLUDE
-</head>
-<body>
-	<div id="container">
-		<canvas id="canvas" width="640" height="480" tabindex="0" oncontextmenu="event.preventDefault();">
-			HTML5 canvas appears to be unsupported in the current browser.<br />
-			Please try updating or use a different browser.
-		</canvas>
-		<div id="status-container">
-			<span id="status" class="godot" onclick="this.style.visibility='hidden';">Downloading page...</span>
-		</div>
-	</div>
-	<div id="output-panel" class="godot">
-		<div id="output-header">
-			Output:
-			<button class="godot" type="button" autocomplete="off" onclick="Presentation.clearOutput();">Clear</button>
-		</div>
-		<div id="output-container"><div id="output-scroll"></div></div>
-	</div>
-
-	<!-- Scripts -->
-	<script type="text/javascript">//<![CDATA[
-		var Presentation = (function() {
-			var statusElement = document.getElementById("status");
-			var canvasElement = document.getElementById("canvas");
-
-			var presentation = {
-				postRun: [],
-				setStatusVisible: function setStatusVisible(visible) {
-					statusElement.style.visibility = (visible ? "visible" : "hidden");
-				},
-				setStatus: function setStatus(text) {
-					if (text.length === 0) {
-						// emscripten sets empty string as status after "Running..."
-						// per timeout, but another status may have been set by then
-						if (Presentation.setStatus.lastText === "Running...")
-							Presentation.setStatusVisible(false);
-						return;
-					}
-					Presentation.setStatus.lastText = text;
-					while (statusElement.lastChild) {
-						statusElement.removeChild(statusElement.lastChild);
-					}
-					var lines = text.split("\n");
-					lines.forEach(function(line, index) {
-						statusElement.appendChild(document.createTextNode(line));
-						statusElement.appendChild(document.createElement("br"));
-					});
-					var closeNote = document.createElement("span");
-					closeNote.style.fontSize = "small";
-					closeNote.textContent = "click to close";
-					statusElement.appendChild(closeNote);
-					Presentation.setStatusVisible(true);
-				},
-				isWebGL2Available: function isWebGL2Available() {
-					var context;
-					try {
-						context = canvasElement.getContext("webgl2") || canvasElement.getContext("experimental-webgl2");
-					} catch (e) {}
-					return !!context;
-				},
-			};
-
-			window.onerror = function(event) { presentation.setStatus("Failure during start-up\nSee JavaScript console") };
-
-			if ($GODOT_DEBUG_ENABLED) { // debugging enabled
-				var outputRoot = document.getElementById("output-panel");
-				var outputElement = document.getElementById("output-scroll");
-				const maxOutputMessages = 400;
-
-				presentation.setOutputVisible = function setOutputVisible(visible) {
-					outputRoot.style.display = (visible ? "block" : "none");
-				};
-				presentation.clearOutput = function clearOutput() {
-					while (outputElement.firstChild) {
-						outputElement.firstChild.remove();
-					}
-				};
-
-				presentation.setOutputVisible(true);
-
-				presentation.print = function print(text) {
-					if (arguments.length > 1) {
-						text = Array.prototype.slice.call(arguments).join(" ");
-					}
-					if (text.length <= 0) return;
-					while (outputElement.childElementCount >= maxOutputMessages) {
-						outputElement.firstChild.remove();
-					}
-					var msg = document.createElement("div");
-					if (String.prototype.trim.call(text).startsWith("**ERROR**")
-						|| String.prototype.trim.call(text).startsWith("**EXCEPTION**")) {
-						msg.style.color = "#d44";
-					} else if (String.prototype.trim.call(text).startsWith("**WARNING**")) {
-						msg.style.color = "#ccc000";
-					} else if (String.prototype.trim.call(text).startsWith("**SCRIPT ERROR**")) {
-						msg.style.color = "#c6d";
-					}
-					msg.textContent = text;
-					var scrollToBottom = outputElement.scrollHeight - (outputElement.clientHeight + outputElement.scrollTop) < 10;
-					outputElement.appendChild(msg);
-					if (scrollToBottom) {
-						outputElement.scrollTop = outputElement.scrollHeight;
-					}
-				};
-
-				presentation.postRun.push(function() {
-					window.onerror = function(event) { presentation.print("**EXCEPTION**:", event) };
-				});
-
-			} else {
-				presentation.postRun.push(function() { window.onerror = null; });
-			}
-
-			return presentation;
-		})();
-
-		// Emscripten interface
-		var Module = (function() {
-			const BASE_NAME = '$GODOT_BASE';
-			var module = {
-				thisProgram: BASE_NAME,
-				wasmBinaryFile: BASE_NAME + '.wasm',
-				TOTAL_MEMORY: $GODOT_TMEM,
-				print: function print(text) {
-					if (arguments.length > 1) {
-						text = Array.prototype.slice.call(arguments).join(" ");
-					}
-					console.log(text);
-					if (typeof Presentation !== "undefined" && typeof Presentation.print === "function") {
-						Presentation.print(text);
-					}
-				},
-				printErr: function printErr(text) {
-					if (arguments.length > 1) {
-						text = Array.prototype.slice.call(arguments).join(" ");
-					}
-					console.error(text);
-					if (typeof Presentation !== "undefined" && typeof Presentation.print === "function") {
-						Presentation.print("**ERROR**:", text)
-					}
-				},
-				canvas: document.getElementById("canvas"),
-				setStatus: function setStatus(text) {
-					var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
-					var now = Date.now();
-					if (m) {
-						if (now - Date.now() < 30) // if this is a progress update, skip it if too soon
-							return;
-						text = m[1];
-					}
-					if (typeof Presentation !== "undefined" && typeof Presentation.setStatus == "function") {
-						Presentation.setStatus(text);
-					}
-				}
-			};
-
-			// As a default initial behavior, pop up an alert when WebGL context is lost. To make your
-			// application robust, you may want to override this behavior before shipping!
-			// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
-			module.canvas.addEventListener("webglcontextlost", function(e) { alert("WebGL context lost. Plase reload the page."); e.preventDefault(); }, false);
-
-			if (typeof Presentation !== "undefined" && Presentation.postRun instanceof Array) {
-				module.postRun = Presentation.postRun;
-			}
-
-			return module;
-		})();
-
-		if (!Presentation.isWebGL2Available()) {
-			Presentation.setStatus("WebGL 2 appears to be unsupported.\nPlease update browser and drivers.");
-			Presentation.preventLoading = true;
-		} else {
-			Presentation.setStatus("Downloading...");
-		}
-
-		if (Presentation.preventLoading) {
-			// prevent *fs.js and Emscripten's SCRIPT placeholder from loading any files
-			Presentation._XHR_send = XMLHttpRequest.prototype.send;
-			XMLHttpRequest.prototype.send = function() {};
-			Presentation._Node_appendChild = Node.prototype.appendChild;
-			Node.prototype.appendChild = function(node) {
-				if (!(node instanceof HTMLScriptElement)) {
-					return Presentation._Node_appendChild.call(this, node);
-				}
-			}
-		}
-	//]]></script>
-	<script type="text/javascript" src="$GODOT_BASEfs.js"></script>
-{{{ SCRIPT }}}
-	<script type="text/javascript">
-		if (Presentation.preventLoading) {
-			XMLHttpRequest.prototype.send = Presentation._XHR_send;
-			Node.prototype.appendChild = Presentation._Node_appendChild;
-		}
-	</script>
-</body>
-</html>

+ 3 - 0
platform/javascript/pre_asmjs.js

@@ -0,0 +1,3 @@
+var Engine = {
+	USING_WASM: false,
+	RuntimeEnvironment: function(Module) {

+ 3 - 0
platform/javascript/pre_wasm.js

@@ -0,0 +1,3 @@
+var Engine = {
+	USING_WASM: true,
+	RuntimeEnvironment: function(Module) {