Browse Source

Merge pull request #39604 from Faless/js/3months_backport

[HTML5 - 3.2] Backport most changes/improvement in master.
Rémi Verschelde 5 years ago
parent
commit
34c5133e6a

+ 3 - 3
modules/webrtc/webrtc_data_channel_js.cpp

@@ -312,14 +312,14 @@ WebRTCDataChannelJS::WebRTCDataChannelJS(int js_id) {
 				return;
 			}
 			var len = buffer.length*buffer.BYTES_PER_ELEMENT;
-			var out = Module._malloc(len);
-			Module.HEAPU8.set(buffer, out);
+			var out = _malloc(len);
+			HEAPU8.set(buffer, out);
 			ccall("_emrtc_on_ch_message",
 				"void",
 				["number", "number", "number", "number"],
 				[c_ptr, out, len, is_string]
 			);
-			Module._free(out);
+			_free(out);
 		}
 
 	}, this, js_id);

+ 3 - 3
modules/websocket/emws_client.cpp

@@ -142,14 +142,14 @@ Error EMWSClient::connect_to_host(String p_host, String p_path, uint16_t p_port,
 
 			}
 			var len = buffer.length*buffer.BYTES_PER_ELEMENT;
-			var out = Module._malloc(len);
-			Module.HEAPU8.set(buffer, out);
+			var out = _malloc(len);
+			HEAPU8.set(buffer, out);
 			ccall("_esws_on_message",
 				"void",
 				["number", "number", "number", "number"],
 				[c_ptr, out, len, is_string]
 			);
-			Module._free(out);
+			_free(out);
 		});
 
 		socket.addEventListener("error", function (event) {

+ 39 - 15
platform/javascript/SCsub

@@ -10,32 +10,56 @@ javascript_files = [
     "os_javascript.cpp",
 ]
 
-build = env.add_program(["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"], javascript_files)
-js, wasm = build
+build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"]
+if env["threads_enabled"]:
+    build_targets.append("#bin/godot${PROGSUFFIX}.worker.js")
+
+build = env.add_program(build_targets, javascript_files)
 
 js_libraries = [
-    "http_request.js",
+    "native/http_request.js",
 ]
 for lib in js_libraries:
     env.Append(LINKFLAGS=["--js-library", env.File(lib).path])
 env.Depends(build, js_libraries)
 
-js_modules = [
-    "id_handler.js",
+js_pre = [
+    "native/id_handler.js",
+    "native/utils.js",
+]
+for js in js_pre:
+    env.Append(LINKFLAGS=["--pre-js", env.File(js).path])
+env.Depends(build, js_pre)
+
+engine = [
+    "engine/preloader.js",
+    "engine/utils.js",
+    "engine/engine.js",
 ]
-for module in js_modules:
-    env.Append(LINKFLAGS=["--pre-js", env.File(module).path])
-env.Depends(build, js_modules)
+externs = [env.File("#platform/javascript/engine/externs.js")]
+js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs)
+env.Depends(js_engine, externs)
 
-wrapper_start = env.File("pre.js")
-wrapper_end = env.File("engine.js")
-js_wrapped = env.Textfile("#bin/godot", [wrapper_start, js, wrapper_end], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js")
+wrap_list = [
+    build[0],
+    js_engine,
+]
+js_wrapped = env.Textfile("#bin/godot", [env.File(f) for f in wrap_list], TEXTFILESUFFIX="${PROGSUFFIX}.wrapped.js")
 
 zip_dir = env.Dir("#bin/.javascript_zip")
-zip_files = env.InstallAs(
-    [zip_dir.File("godot.js"), zip_dir.File("godot.wasm"), zip_dir.File("godot.html")],
-    [js_wrapped, wasm, "#misc/dist/html/full-size.html"],
-)
+binary_name = "godot.tools" if env["tools"] else "godot"
+out_files = [
+    zip_dir.File(binary_name + ".js"),
+    zip_dir.File(binary_name + ".wasm"),
+    zip_dir.File(binary_name + ".html"),
+]
+html_file = "#misc/dist/html/full-size.html"
+in_files = [js_wrapped, build[1], html_file]
+if env["threads_enabled"]:
+    in_files.append(build[2])
+    out_files.append(zip_dir.File(binary_name + ".worker.js"))
+
+zip_files = env.InstallAs(out_files, in_files)
 env.Zip(
     "#bin/godot",
     zip_files,

+ 84 - 60
platform/javascript/audio_driver_javascript.cpp

@@ -37,22 +37,18 @@
 AudioDriverJavaScript *AudioDriverJavaScript::singleton = NULL;
 
 const char *AudioDriverJavaScript::get_name() const {
-
 	return "JavaScript";
 }
 
 extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_js_mix() {
-
 	AudioDriverJavaScript::singleton->mix_to_js();
 }
 
 extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) {
-
 	AudioDriverJavaScript::singleton->process_capture(sample);
 }
 
 void AudioDriverJavaScript::mix_to_js() {
-
 	int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode());
 	int sample_count = memarr_len(internal_buffer) / channel_count;
 	int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer);
@@ -63,24 +59,24 @@ void AudioDriverJavaScript::mix_to_js() {
 }
 
 void AudioDriverJavaScript::process_capture(float sample) {
-
 	int32_t sample32 = int32_t(sample * 32768.f) * (1U << 16);
 	input_buffer_write(sample32);
 }
 
 Error AudioDriverJavaScript::init() {
-
 	int mix_rate = GLOBAL_GET("audio/mix_rate");
 	int latency = GLOBAL_GET("audio/output_latency");
 
 	/* clang-format off */
-	EM_ASM({
+	_driver_id = EM_ASM_INT({
 		const MIX_RATE = $0;
 		const LATENCY = $1 / 1000;
-		_audioDriver_audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: MIX_RATE, latencyHint: LATENCY});
-		_audioDriver_audioInput = null;
-		_audioDriver_inputStream = null;
-		_audioDriver_scriptNode = null;
+		return Module.IDHandler.add({
+			'context': new (window.AudioContext || window.webkitAudioContext)({ sampleRate: MIX_RATE, latencyHint: LATENCY}),
+			'input': null,
+			'stream': null,
+			'script': null
+		});
 	}, mix_rate, latency);
 	/* clang-format on */
 
@@ -88,14 +84,16 @@ Error AudioDriverJavaScript::init() {
 	buffer_length = closest_power_of_2((latency * mix_rate / 1000) * channel_count);
 	/* clang-format off */
 	buffer_length = EM_ASM_INT({
-		const BUFFER_LENGTH = $0;
-		const CHANNEL_COUNT = $1;
-
-		_audioDriver_scriptNode = _audioDriver_audioContext.createScriptProcessor(BUFFER_LENGTH, 2, CHANNEL_COUNT);
-		_audioDriver_scriptNode.connect(_audioDriver_audioContext.destination);
-
-		return _audioDriver_scriptNode.bufferSize;
-	}, buffer_length, channel_count);
+		var ref = Module.IDHandler.get($0);
+		const ctx = ref['context'];
+		const BUFFER_LENGTH = $1;
+		const CHANNEL_COUNT = $2;
+
+		var script = ctx.createScriptProcessor(BUFFER_LENGTH, 2, CHANNEL_COUNT);
+		script.connect(ctx.destination);
+		ref['script'] = script;
+		return script.bufferSize;
+	}, _driver_id, buffer_length, channel_count);
 	/* clang-format on */
 	if (!buffer_length) {
 		return FAILED;
@@ -111,14 +109,14 @@ Error AudioDriverJavaScript::init() {
 }
 
 void AudioDriverJavaScript::start() {
-
 	/* clang-format off */
 	EM_ASM({
-		var INTERNAL_BUFFER_PTR = $0;
+		const ref = Module.IDHandler.get($0);
+		var INTERNAL_BUFFER_PTR = $1;
 
 		var audioDriverMixFunction = cwrap('audio_driver_js_mix');
 		var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']);
-		_audioDriver_scriptNode.onaudioprocess = function(audioProcessingEvent) {
+		ref['script'].onaudioprocess = function(audioProcessingEvent) {
 			audioDriverMixFunction();
 
 			var input = audioProcessingEvent.inputBuffer;
@@ -135,7 +133,7 @@ void AudioDriverJavaScript::start() {
 				}
 			}
 
-			if (_audioDriver_audioInput) {
+			if (ref['input']) {
 				var inputDataL = input.getChannelData(0);
 				var inputDataR = input.getChannelData(1);
 				for (var i = 0; i < inputDataL.length; i++) {
@@ -144,51 +142,54 @@ void AudioDriverJavaScript::start() {
 				}
 			}
 		};
-	}, internal_buffer);
+	}, _driver_id, internal_buffer);
 	/* clang-format on */
 }
 
 void AudioDriverJavaScript::resume() {
 	/* clang-format off */
 	EM_ASM({
-		if (_audioDriver_audioContext.resume)
-			_audioDriver_audioContext.resume();
-	});
+		const ref = Module.IDHandler.get($0);
+		if (ref && ref['context'] && ref['context'].resume)
+			ref['context'].resume();
+	}, _driver_id);
 	/* clang-format on */
 }
 
 float AudioDriverJavaScript::get_latency() {
 	/* clang-format off */
 	return EM_ASM_DOUBLE({
+		const ref = Module.IDHandler.get($0);
 		var latency = 0;
-		if (_audioDriver_audioContext) {
-			if (_audioDriver_audioContext.baseLatency) {
-				latency += _audioDriver_audioContext.baseLatency;
+		if (ref && ref['context']) {
+			const ctx = ref['context'];
+			if (ctx.baseLatency) {
+				latency += ctx.baseLatency;
 			}
-			if (_audioDriver_audioContext.outputLatency) {
-				latency += _audioDriver_audioContext.outputLatency;
+			if (ctx.outputLatency) {
+				latency += ctx.outputLatency;
 			}
 		}
 		return latency;
-	});
+	}, _driver_id);
 	/* clang-format on */
 }
 
 int AudioDriverJavaScript::get_mix_rate() const {
-
 	/* clang-format off */
-	return EM_ASM_INT_V({
-		return _audioDriver_audioContext.sampleRate;
-	});
+	return EM_ASM_INT({
+		const ref = Module.IDHandler.get($0);
+		return ref && ref['context'] ? ref['context'].sampleRate : 0;
+	}, _driver_id);
 	/* clang-format on */
 }
 
 AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const {
-
 	/* clang-format off */
-	return get_speaker_mode_by_total_channels(EM_ASM_INT_V({
-		return _audioDriver_audioContext.destination.channelCount;
-	}));
+	return get_speaker_mode_by_total_channels(EM_ASM_INT({
+		const ref = Module.IDHandler.get($0);
+		return ref && ref['context'] ? ref['context'].destination.channelCount : 0;
+	}, _driver_id));
 	/* clang-format on */
 }
 
@@ -199,16 +200,38 @@ void AudioDriverJavaScript::lock() {
 void AudioDriverJavaScript::unlock() {
 }
 
-void AudioDriverJavaScript::finish() {
+void AudioDriverJavaScript::finish_async() {
+	// Close the context, add the operation to the async_finish list in module.
+	int id = _driver_id;
+	_driver_id = 0;
 
 	/* clang-format off */
 	EM_ASM({
-		_audioDriver_audioContext = null;
-		_audioDriver_audioInput = null;
-		_audioDriver_scriptNode = null;
-	});
+		var ref = Module.IDHandler.get($0);
+		Module.async_finish.push(new Promise(function(accept, reject) {
+			if (!ref) {
+				console.log("Ref not found!", $0, Module.IDHandler);
+				setTimeout(accept, 0);
+			} else {
+				const context = ref['context'];
+				// Disconnect script and input.
+				ref['script'].disconnect();
+				if (ref['input'])
+					ref['input'].disconnect();
+				ref = null;
+				context.close().then(function() {
+					accept();
+				}).catch(function(e) {
+					accept();
+				});
+			}
+		}));
+		Module.IDHandler.remove($0);
+	}, id);
 	/* clang-format on */
+}
 
+void AudioDriverJavaScript::finish() {
 	if (internal_buffer) {
 		memdelete_arr(internal_buffer);
 		internal_buffer = NULL;
@@ -216,15 +239,15 @@ void AudioDriverJavaScript::finish() {
 }
 
 Error AudioDriverJavaScript::capture_start() {
-
 	input_buffer_init(buffer_length);
 
 	/* clang-format off */
 	EM_ASM({
 		function gotMediaInput(stream) {
-			_audioDriver_inputStream = stream;
-			_audioDriver_audioInput = _audioDriver_audioContext.createMediaStreamSource(stream);
-			_audioDriver_audioInput.connect(_audioDriver_scriptNode);
+			var ref = Module.IDHandler.get($0);
+			ref['stream'] = stream;
+			ref['input'] = ref['context'].createMediaStreamSource(stream);
+			ref['input'].connect(ref['script']);
 		}
 
 		function gotMediaInputError(e) {
@@ -238,30 +261,30 @@ Error AudioDriverJavaScript::capture_start() {
 				navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
 			navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError);
 		}
-	});
+	}, _driver_id);
 	/* clang-format on */
 
 	return OK;
 }
 
 Error AudioDriverJavaScript::capture_stop() {
-
 	/* clang-format off */
 	EM_ASM({
-		if (_audioDriver_inputStream) {
-			const tracks = _audioDriver_inputStream.getTracks();
+		var ref = Module.IDHandler.get($0);
+		if (ref['stream']) {
+			const tracks = ref['stream'].getTracks();
 			for (var i = 0; i < tracks.length; i++) {
 				tracks[i].stop();
 			}
-			_audioDriver_inputStream = null;
+			ref['stream'] = null;
 		}
 
-		if (_audioDriver_audioInput) {
-			_audioDriver_audioInput.disconnect();
-			_audioDriver_audioInput = null;
+		if (ref['input']) {
+			ref['input'].disconnect();
+			ref['input'] = null;
 		}
 
-	});
+	}, _driver_id);
 	/* clang-format on */
 
 	input_buffer.clear();
@@ -270,8 +293,9 @@ Error AudioDriverJavaScript::capture_stop() {
 }
 
 AudioDriverJavaScript::AudioDriverJavaScript() {
-
+	_driver_id = 0;
 	internal_buffer = NULL;
+	buffer_length = 0;
 
 	singleton = this;
 }

+ 2 - 0
platform/javascript/audio_driver_javascript.h

@@ -37,6 +37,7 @@ class AudioDriverJavaScript : public AudioDriver {
 
 	float *internal_buffer;
 
+	int _driver_id;
 	int buffer_length;
 
 public:
@@ -56,6 +57,7 @@ public:
 	virtual void lock();
 	virtual void unlock();
 	virtual void finish();
+	void finish_async();
 
 	virtual Error capture_start();
 	virtual Error capture_stop();

+ 67 - 57
platform/javascript/detect.py

@@ -1,5 +1,7 @@
 import os
 
+from emscripten_helpers import parse_config, run_closure_compiler, create_engine_file
+
 
 def is_active():
     return True
@@ -19,6 +21,8 @@ def get_opts():
     return [
         # eval() can be a security concern, so it can be disabled.
         BoolVariable("javascript_eval", "Enable JavaScript eval interface", True),
+        BoolVariable("threads_enabled", "Enable WebAssembly Threads support (limited browser support)", False),
+        BoolVariable("use_closure_compiler", "Use closure compiler to minimize JavaScript code", False),
     ]
 
 
@@ -38,7 +42,7 @@ def configure(env):
 
     ## Build type
 
-    if env["target"] != "debug":
+    if env["target"] == "release":
         # Use -Os to prioritize optimizing for reduced file size. This is
         # particularly valuable for the web platform because it directly
         # decreases download time.
@@ -47,40 +51,57 @@ def configure(env):
         # run-time performance.
         env.Append(CCFLAGS=["-Os"])
         env.Append(LINKFLAGS=["-Os"])
-        if env["target"] == "release_debug":
-            env.Append(CPPDEFINES=["DEBUG_ENABLED"])
-            # Retain function names for backtraces at the cost of file size.
-            env.Append(LINKFLAGS=["--profiling-funcs"])
-    else:
+    elif env["target"] == "release_debug":
+        env.Append(CCFLAGS=["-Os"])
+        env.Append(LINKFLAGS=["-Os"])
+        env.Append(CPPDEFINES=["DEBUG_ENABLED"])
+        # Retain function names for backtraces at the cost of file size.
+        env.Append(LINKFLAGS=["--profiling-funcs"])
+    else:  # "debug"
         env.Append(CPPDEFINES=["DEBUG_ENABLED"])
         env.Append(CCFLAGS=["-O1", "-g"])
         env.Append(LINKFLAGS=["-O1", "-g"])
         env.Append(LINKFLAGS=["-s", "ASSERTIONS=1"])
 
-    ## Compiler configuration
+    if env["tools"]:
+        if not env["threads_enabled"]:
+            raise RuntimeError(
+                "Threads must be enabled to build the editor. Please add the 'threads_enabled=yes' option"
+            )
+        # Tools need more memory. Initial stack memory in bytes. See `src/settings.js` in emscripten repository (will be renamed to INITIAL_MEMORY).
+        env.Append(LINKFLAGS=["-s", "TOTAL_MEMORY=33554432"])
+    else:
+        # Disable exceptions and rtti on non-tools (template) builds
+        # These flags help keep the file size down.
+        env.Append(CCFLAGS=["-fno-exceptions", "-fno-rtti"])
+        # Don't use dynamic_cast, necessary with no-rtti.
+        env.Append(CPPDEFINES=["NO_SAFE_CAST"])
 
+    ## Copy env variables.
     env["ENV"] = os.environ
 
-    em_config_file = os.getenv("EM_CONFIG") or os.path.expanduser("~/.emscripten")
-    if not os.path.exists(em_config_file):
-        raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file)
-    with open(em_config_file) as f:
-        em_config = {}
-        try:
-            # Emscripten configuration file is a Python file with simple assignments.
-            exec(f.read(), em_config)
-        except StandardError as e:
-            raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e))
-    if "BINARYEN_ROOT" in em_config and os.path.isdir(os.path.join(em_config.get("BINARYEN_ROOT"), "emscripten")):
-        # New style, emscripten path as a subfolder of BINARYEN_ROOT
-        env.PrependENVPath("PATH", os.path.join(em_config.get("BINARYEN_ROOT"), "emscripten"))
-    elif "EMSCRIPTEN_ROOT" in em_config:
-        # Old style (but can be there as a result from previous activation, so do last)
-        env.PrependENVPath("PATH", em_config.get("EMSCRIPTEN_ROOT"))
-    else:
-        raise RuntimeError(
-            "'BINARYEN_ROOT' or 'EMSCRIPTEN_ROOT' missing in Emscripten configuration file '%s'" % em_config_file
-        )
+    # LTO
+    if env["use_lto"]:
+        env.Append(CCFLAGS=["-s", "WASM_OBJECT_FILES=0"])
+        env.Append(LINKFLAGS=["-s", "WASM_OBJECT_FILES=0"])
+        env.Append(LINKFLAGS=["--llvm-lto", "1"])
+
+    # Closure compiler
+    if env["use_closure_compiler"]:
+        # For emscripten support code.
+        env.Append(LINKFLAGS=["--closure", "1"])
+        # Register builder for our Engine files
+        jscc = env.Builder(generator=run_closure_compiler, suffix=".cc.js", src_suffix=".js")
+        env.Append(BUILDERS={"BuildJS": jscc})
+
+    # Add method that joins/compiles our Engine files.
+    env.AddMethod(create_engine_file, "CreateEngineFile")
+
+    # Closure compiler extern and support for ecmascript specs (const, let, etc).
+    env["ENV"]["EMCC_CLOSURE_ARGS"] = "--language_in ECMASCRIPT6"
+
+    em_config = parse_config()
+    env.PrependENVPath("PATH", em_config["EMCC_ROOT"])
 
     env["CC"] = "emcc"
     env["CXX"] = "em++"
@@ -105,44 +126,31 @@ def configure(env):
     env["LIBPREFIXES"] = ["$LIBPREFIX"]
     env["LIBSUFFIXES"] = ["$LIBSUFFIX"]
 
-    ## Compile flags
-
     env.Prepend(CPPPATH=["#platform/javascript"])
     env.Append(CPPDEFINES=["JAVASCRIPT_ENABLED", "UNIX_ENABLED"])
 
-    # No multi-threading (SharedArrayBuffer) available yet,
-    # once feasible also consider memory buffer size issues.
-    env.Append(CPPDEFINES=["NO_THREADS"])
-
-    # Disable exceptions and rtti on non-tools (template) builds
-    if not env["tools"]:
-        # These flags help keep the file size down.
-        env.Append(CCFLAGS=["-fno-exceptions", "-fno-rtti"])
-        # Don't use dynamic_cast, necessary with no-rtti.
-        env.Append(CPPDEFINES=["NO_SAFE_CAST"])
-
     if env["javascript_eval"]:
         env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"])
 
-    ## Link flags
+    # Thread support (via SharedArrayBuffer).
+    if env["threads_enabled"]:
+        env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"])
+        env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"])
+        env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"])
+        env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=4"])
+        env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"])
+    else:
+        env.Append(CPPDEFINES=["NO_THREADS"])
+
+    # Reduce code size by generating less support code (e.g. skip NodeJS support).
+    env.Append(LINKFLAGS=["-s", "ENVIRONMENT=web,worker"])
 
     # We use IDBFS in javascript_main.cpp. Since Emscripten 1.39.1 it needs to
     # be linked explicitly.
     env.Append(LIBS=["idbfs.js"])
 
     env.Append(LINKFLAGS=["-s", "BINARYEN=1"])
-
-    # Only include the JavaScript support code for the web environment
-    # (i.e. exclude Node.js and other unused environments).
-    # This makes the JavaScript support code about 4 KB smaller.
-    env.Append(LINKFLAGS=["-s", "ENVIRONMENT=web"])
-
-    # This needs to be defined for Emscripten using 'fastcomp' (default pre-1.39.0)
-    # and undefined if using 'upstream'. And to make things simple, earlier
-    # Emscripten versions didn't include 'fastcomp' in their path, so we check
-    # against the presence of 'upstream' to conditionally add the flag.
-    if not "upstream" in em_config["EMSCRIPTEN_ROOT"]:
-        env.Append(LINKFLAGS=["-s", "BINARYEN_TRAP_MODE='clamp'"])
+    env.Append(LINKFLAGS=["-s", "MODULARIZE=1", "-s", "EXPORT_NAME='Godot'"])
 
     # Allow increasing memory buffer size during runtime. This is efficient
     # when using WebAssembly (in comparison to asm.js) and works well for
@@ -154,8 +162,10 @@ def configure(env):
 
     env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"])
 
-    # TODO: Reevaluate usage of this setting now that engine.js manages engine runtime.
-    env.Append(LINKFLAGS=["-s", "NO_EXIT_RUNTIME=1"])
+    # Allow use to take control of swapping WebGL buffers.
+    env.Append(LINKFLAGS=["-s", "OFFSCREEN_FRAMEBUFFER=1"])
 
-    # adding flag due to issue with emscripten 1.38.41 callMain method https://github.com/emscripten-core/emscripten/blob/incoming/ChangeLog.md#v13841-08072019
-    env.Append(LINKFLAGS=["-s", 'EXTRA_EXPORTED_RUNTIME_METHODS=["callMain"]'])
+    # callMain for manual start, FS for preloading, PATH and ERRNO_CODES for BrowserFS.
+    env.Append(LINKFLAGS=["-s", "EXTRA_EXPORTED_RUNTIME_METHODS=['callMain', 'FS']"])
+    # Add code that allow exiting runtime.
+    env.Append(LINKFLAGS=["-s", "EXIT_RUNTIME=1"])

+ 38 - 0
platform/javascript/emscripten_helpers.py

@@ -0,0 +1,38 @@
+import os
+
+
+def parse_config():
+    em_config_file = os.getenv("EM_CONFIG") or os.path.expanduser("~/.emscripten")
+    if not os.path.exists(em_config_file):
+        raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file)
+
+    normalized = {}
+    em_config = {}
+    with open(em_config_file) as f:
+        try:
+            # Emscripten configuration file is a Python file with simple assignments.
+            exec(f.read(), em_config)
+        except StandardError as e:
+            raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e))
+    normalized["EMCC_ROOT"] = em_config.get("EMSCRIPTEN_ROOT")
+    normalized["NODE_JS"] = em_config.get("NODE_JS")
+    normalized["CLOSURE_BIN"] = os.path.join(normalized["EMCC_ROOT"], "node_modules", ".bin", "google-closure-compiler")
+    return normalized
+
+
+def run_closure_compiler(target, source, env, for_signature):
+    cfg = parse_config()
+    cmd = [cfg["NODE_JS"], cfg["CLOSURE_BIN"]]
+    cmd.extend(["--compilation_level", "ADVANCED_OPTIMIZATIONS"])
+    for f in env["JSEXTERNS"]:
+        cmd.extend(["--externs", f.get_abspath()])
+    for f in source:
+        cmd.extend(["--js", f.get_abspath()])
+    cmd.extend(["--js_output_file", target[0].get_abspath()])
+    return " ".join(cmd)
+
+
+def create_engine_file(env, target, source, externs):
+    if env["use_closure_compiler"]:
+        return env.BuildJS(target, source, JSEXTERNS=externs)
+    return env.Textfile(target, [env.File(s) for s in source])

+ 0 - 411
platform/javascript/engine.js

@@ -1,411 +0,0 @@
-		// The following is concatenated with generated code, and acts as the end
-		// of a wrapper for said code. See pre.js for the other part of the
-		// wrapper.
-		exposedLibs['PATH'] = PATH;
-		exposedLibs['FS'] = FS;
-		return Module;
-	},
-};
-
-(function() {
-	var engine = Engine;
-
-	var DOWNLOAD_ATTEMPTS_MAX = 4;
-
-	var basePath = null;
-	var wasmFilenameExtensionOverride = null;
-	var engineLoadPromise = null;
-
-	var loadingFiles = {};
-
-	function getPathLeaf(path) {
-
-		while (path.endsWith('/'))
-			path = path.slice(0, -1);
-		return path.slice(path.lastIndexOf('/') + 1);
-	}
-
-	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) {
-
-		return getPathLeaf(getBasePath(path));
-	}
-
-	Engine = function Engine() {
-
-		this.rtenv = null;
-
-		var LIBS = {};
-
-		var initPromise = null;
-		var unloadAfterInit = true;
-
-		var preloadedFiles = [];
-
-		var resizeCanvasOnStart = true;
-		var progressFunc = null;
-		var preloadProgressTracker = {};
-		var lastProgress = { loaded: 0, total: 0 };
-
-		var canvas = null;
-		var executableName = null;
-		var locale = null;
-		var stdout = null;
-		var stderr = null;
-
-		this.init = function(newBasePath) {
-
-			if (!initPromise) {
-				initPromise = Engine.load(newBasePath).then(
-					instantiate.bind(this)
-				);
-				requestAnimationFrame(animateProgress);
-				if (unloadAfterInit)
-					initPromise.then(Engine.unloadEngine);
-			}
-			return initPromise;
-		};
-
-		function instantiate(wasmBuf) {
-
-			var rtenvProps = {
-				engine: this,
-				ENV: {},
-			};
-			if (typeof stdout === 'function')
-				rtenvProps.print = stdout;
-			if (typeof stderr === 'function')
-				rtenvProps.printErr = stderr;
-			rtenvProps.instantiateWasm = function(imports, onSuccess) {
-				WebAssembly.instantiate(wasmBuf, imports).then(function(result) {
-					onSuccess(result.instance);
-				});
-				return {};
-			};
-
-			return new Promise(function(resolve, reject) {
-				rtenvProps.onRuntimeInitialized = resolve;
-				rtenvProps.onAbort = reject;
-				rtenvProps.thisProgram = executableName;
-				rtenvProps.engine.rtenv = Engine.RuntimeEnvironment(rtenvProps, LIBS);
-			});
-		}
-
-		this.preloadFile = 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) {
-				preloadedFiles.push({
-					path: destPath,
-					buffer: pathOrBuffer
-				});
-				return Promise.resolve();
-			} else if (typeof pathOrBuffer === 'string') {
-				return loadPromise(pathOrBuffer, preloadProgressTracker).then(function(xhr) {
-					preloadedFiles.push({
-						path: destPath || pathOrBuffer,
-						buffer: xhr.response
-					});
-				});
-			} else {
-				throw Promise.reject("Invalid object for preloading");
-			}
-		};
-
-		this.start = function() {
-
-			return this.init().then(
-				Function.prototype.apply.bind(synchronousStart, this, arguments)
-			);
-		};
-
-		this.startGame = function(execName, mainPack) {
-
-			executableName = execName;
-			var mainArgs = [ '--main-pack', getPathLeaf(mainPack) ];
-
-			return Promise.all([
-				this.init(getBasePath(execName)),
-				this.preloadFile(mainPack, getPathLeaf(mainPack))
-			]).then(
-				Function.prototype.apply.bind(synchronousStart, this, mainArgs)
-			);
-		};
-
-		function synchronousStart() {
-
-			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;
-			// 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';
-			// disable right-click context menu
-			actualCanvas.addEventListener('contextmenu', function(ev) {
-				ev.preventDefault();
-			}, false);
-			// until context restoration is implemented
-			actualCanvas.addEventListener('webglcontextlost', function(ev) {
-				alert("WebGL context lost, please reload the page");
-				ev.preventDefault();
-			}, false);
-
-			if (locale) {
-				this.rtenv.locale = locale;
-			} else {
-				this.rtenv.locale = navigator.languages ? navigator.languages[0] : navigator.language;
-			}
-			this.rtenv.locale = this.rtenv.locale.split('.')[0];
-			this.rtenv.resizeCanvasOnStart = resizeCanvasOnStart;
-
-			preloadedFiles.forEach(function(file) {
-				var dir = LIBS.PATH.dirname(file.path);
-				try {
-					LIBS.FS.stat(dir);
-				} catch (e) {
-					if (e.code !== 'ENOENT') {
-						throw e;
-					}
-					LIBS.FS.mkdirTree(dir);
-				}
-				// With memory growth, canOwn should be false.
-				LIBS.FS.createDataFile(file.path, null, new Uint8Array(file.buffer), true, true, false);
-			}, this);
-
-			preloadedFiles = null;
-			initPromise = null;
-			this.rtenv.callMain(arguments);
-		}
-
-		this.setProgressFunc = function(func) {
-			progressFunc = func;
-		};
-
-		this.setResizeCanvasOnStart = function(enabled) {
-			resizeCanvasOnStart = enabled;
-		};
-
-		function animateProgress() {
-
-			var loaded = 0;
-			var total = 0;
-			var totalIsValid = true;
-			var progressIsFinal = true;
-
-			[loadingFiles, preloadProgressTracker].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.setExecutableName = function(newName) {
-
-			executableName = newName;
-		};
-
-		this.setLocale = function(newLocale) {
-
-			locale = newLocale;
-		};
-
-		this.setUnloadAfterInit = function(enabled) {
-
-			if (enabled && !unloadAfterInit && initPromise) {
-				initPromise.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.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;
-	};
-
-	Engine.setWebAssemblyFilenameExtension = function(override) {
-
-		if (String(override).length === 0) {
-			throw new Error('Invalid WebAssembly filename extension override');
-		}
-		wasmFilenameExtensionOverride = String(override);
-	}
-
-	Engine.load = function(newBasePath) {
-
-		if (newBasePath !== undefined) basePath = getBasePath(newBasePath);
-		if (engineLoadPromise === null) {
-			if (typeof WebAssembly !== 'object')
-				return Promise.reject(new Error("Browser doesn't support WebAssembly"));
-			// TODO cache/retrieve module to/from idb
-			engineLoadPromise = loadPromise(basePath + '.' + (wasmFilenameExtensionOverride || 'wasm')).then(function(xhr) {
-				return xhr.response;
-			});
-			engineLoadPromise = engineLoadPromise.catch(function(err) {
-				engineLoadPromise = null;
-				throw err;
-			});
-		}
-		return engineLoadPromise;
-	};
-
-	Engine.unload = 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', '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;
-		}
-	}
-})();

+ 245 - 0
platform/javascript/engine/engine.js

@@ -0,0 +1,245 @@
+Function('return this')()['Engine'] = (function() {
+	var preloader = new Preloader();
+
+	var wasmExt = '.wasm';
+	var unloadAfterInit = true;
+	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() {
+		this.canvas = null;
+		this.executableName = '';
+		this.rtenv = null;
+		this.customLocale = null;
+		this.resizeCanvasOnStart = false;
+		this.onExecute = null;
+		this.onExit = null;
+	};
+
+	Engine.prototype.init = /** @param {string=} basePath */ function(basePath) {
+		if (initPromise) {
+			return initPromise;
+		}
+		if (loadPromise == null) {
+			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;
+		var me = this;
+		initPromise = new Promise(function(resolve, reject) {
+			config['locateFile'] = Utils.createLocateRewrite(loadPath);
+			config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise);
+			Godot(config).then(function(module) {
+				me.rtenv = module;
+				if (unloadAfterInit) {
+					unload();
+				}
+				resolve();
+				config = null;
+			});
+		});
+		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 me.init().then(function() {
+			if (!me.rtenv) {
+				return Promise.reject(new Error('The engine must be initialized before it can be started'));
+			}
+
+			if (!(me.canvas instanceof HTMLCanvasElement)) {
+				me.canvas = Utils.findCanvas();
+			}
+
+			// Canvas can grab focus on click, or key events won't work.
+			if (me.canvas.tabIndex < 0) {
+				me.canvas.tabIndex = 0;
+			}
+
+			// Disable right-click context menu.
+			me.canvas.addEventListener('contextmenu', function(ev) {
+				ev.preventDefault();
+			}, false);
+
+			// Until context restoration is implemented warn the user of context loss.
+			me.canvas.addEventListener('webglcontextlost', function(ev) {
+				alert("WebGL context lost, please reload the page");
+				ev.preventDefault();
+			}, false);
+
+			// Browser locale, or custom one if defined.
+			var locale = me.customLocale;
+			if (!locale) {
+				locale = navigator.languages ? navigator.languages[0] : navigator.language;
+				locale = locale.split('.')[0];
+			}
+			me.rtenv['locale'] = locale;
+			me.rtenv['canvas'] = me.canvas;
+			me.rtenv['thisProgram'] = me.executableName;
+			me.rtenv['resizeCanvasOnStart'] = me.resizeCanvasOnStart;
+			me.rtenv['noExitRuntime'] = true;
+			me.rtenv['onExecute'] = me.onExecute;
+			me.rtenv['onExit'] = function(code) {
+				if (me.onExit)
+					me.onExit(code);
+				me.rtenv = null;
+			}
+			return new Promise(function(resolve, reject) {
+				preloader.preloadedFiles.forEach(function(file) {
+					me.rtenv['copyToFS'](file.path, file.buffer);
+				});
+				preloader.preloadedFiles.length = 0; // Clear memory
+				me.rtenv['callMain'](args);
+				initPromise = null;
+				resolve();
+			});
+		});
+	};
+
+	Engine.prototype.startGame = function(execName, mainPack, extraArgs) {
+		// Start and init with execName as loadPath if not inited.
+		this.executableName = execName;
+		var me = this;
+		return Promise.all([
+			this.init(execName),
+			this.preloadFile(mainPack, mainPack)
+		]).then(function() {
+			var args = ['--main-pack', mainPack];
+			if (extraArgs)
+				args = args.concat(extraArgs);
+			return me.start.apply(me, args);
+		});
+	};
+
+	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) {
+		this.canvas = canvasElem;
+	};
+
+	Engine.prototype.setCanvasResizedOnStart = function(enabled) {
+		this.resizeCanvasOnStart = enabled;
+	};
+
+	Engine.prototype.setLocale = function(locale) {
+		this.customLocale = locale;
+	};
+
+	Engine.prototype.setExecutableName = function(newName) {
+		this.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 (this.rtenv)
+			this.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 (this.rtenv)
+			this.rtenv.printErr = printErr;
+		stderr = printErr;
+	};
+
+	Engine.prototype.setOnExecute = function(onExecute) {
+		if (this.rtenv)
+			this.rtenv.onExecute = onExecute;
+		this.onExecute = onExecute;
+	}
+
+	Engine.prototype.setOnExit = function(onExit) {
+		this.onExit = onExit;
+	}
+
+	Engine.prototype.copyToFS = function(path, buffer) {
+		if (this.rtenv == null) {
+			throw new Error("Engine must be inited before copying files");
+		}
+		this.rtenv['copyToFS'](path, buffer);
+	}
+
+	// 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;
+	Engine.prototype['setOnExecute'] = Engine.prototype.setOnExecute;
+	Engine.prototype['setOnExit'] = Engine.prototype.setOnExit;
+	Engine.prototype['copyToFS'] = Engine.prototype.copyToFS;
+	return Engine;
+})();

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

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

+ 139 - 0
platform/javascript/engine/preloader.js

@@ -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;
+	}
+};

+ 51 - 0
platform/javascript/engine/utils.js

@@ -0,0 +1,51 @@
+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;
+	},
+
+	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;
+	}
+};

+ 8 - 0
platform/javascript/export/export.cpp

@@ -94,6 +94,9 @@ public:
 		} else if (req[1] == basereq + ".js") {
 			filepath += ".js";
 			ctype = "application/javascript";
+		} else if (req[1] == basereq + ".worker.js") {
+			filepath += ".worker.js";
+			ctype = "application/javascript";
 		} else if (req[1] == basereq + ".pck") {
 			filepath += ".pck";
 			ctype = "application/octet-stream";
@@ -435,6 +438,10 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 		} else if (file == "godot.js") {
 
 			file = p_path.get_file().get_basename() + ".js";
+		} else if (file == "godot.worker.js") {
+
+			file = p_path.get_file().get_basename() + ".worker.js";
+
 		} else if (file == "godot.wasm") {
 
 			file = p_path.get_file().get_basename() + ".wasm";
@@ -568,6 +575,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese
 		// Export generates several files, clean them up on failure.
 		DirAccess::remove_file_or_error(basepath + ".html");
 		DirAccess::remove_file_or_error(basepath + ".js");
+		DirAccess::remove_file_or_error(basepath + ".worker.js");
 		DirAccess::remove_file_or_error(basepath + ".pck");
 		DirAccess::remove_file_or_error(basepath + ".png");
 		DirAccess::remove_file_or_error(basepath + ".wasm");

+ 75 - 4
platform/javascript/javascript_main.cpp

@@ -34,27 +34,98 @@
 
 #include <emscripten/emscripten.h>
 
+static OS_JavaScript *os = NULL;
+
+// Files drop (implemented in JS for now).
+extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p_filec) {
+	if (!os || !os->get_main_loop()) {
+		ERR_FAIL_MSG("Unable to drop files because the OS or MainLoop are not active");
+	}
+	Vector<String> files;
+	for (int i = 0; i < p_filec; i++) {
+		files.push_back(String::utf8(p_filev[i]));
+	}
+	os->get_main_loop()->drop_files(files);
+}
+
+void exit_callback() {
+	emscripten_cancel_main_loop(); // After this, we can exit!
+	Main::cleanup();
+	int exit_code = OS_JavaScript::get_singleton()->get_exit_code();
+	memdelete(os);
+	os = NULL;
+	emscripten_force_exit(exit_code); // No matter that we call cancel_main_loop, regular "exit" will not work, forcing.
+}
+
+void main_loop_callback() {
+
+	if (os->main_loop_iterate()) {
+		emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async.
+		EM_ASM({
+			// This will contain the list of operations that need to complete before cleanup.
+			Module.async_finish = [];
+		});
+		os->get_main_loop()->finish();
+		os->finalize_async(); // Will add all the async finish functions.
+		EM_ASM({
+			Promise.all(Module.async_finish).then(function() {
+				Module.async_finish = [];
+				ccall("cleanup_after_sync", null, []);
+			});
+		});
+	}
+}
+
+extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() {
+	emscripten_set_main_loop(exit_callback, -1, false);
+}
+
 extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) {
 
+	OS_JavaScript *os = OS_JavaScript::get_singleton();
+
+	// Set IDBFS status
 	String idbfs_err = String::utf8(p_idbfs_err);
 	if (!idbfs_err.empty()) {
 		print_line("IndexedDB not available: " + idbfs_err);
 	}
-	OS_JavaScript *os = OS_JavaScript::get_singleton();
 	os->set_idb_available(idbfs_err.empty());
+
+	// Set canvas ID
+	char canvas_ptr[256];
+	/* clang-format off */
+	EM_ASM({
+		stringToUTF8("#" + Module['canvas'].id, $0, 255);
+	}, canvas_ptr);
+	/* clang-format on */
+	os->canvas_id.parse_utf8(canvas_ptr, 255);
+
+	// Set locale
+	char locale_ptr[16];
+	/* clang-format off */
+	EM_ASM({
+		stringToUTF8(Module['locale'], $0, 16);
+	}, locale_ptr);
+
+	/* clang-format on */
+	setenv("LANG", locale_ptr, true);
+
 	Main::setup2();
 	// Ease up compatibility.
 	ResourceLoader::set_abort_on_missing_resources(false);
 	Main::start();
-	os->run_async();
-	os->main_loop_iterate();
+	os->get_main_loop()->init();
+	main_loop_callback();
+	emscripten_resume_main_loop();
 }
 
 int main(int argc, char *argv[]) {
 
-	new OS_JavaScript(argc, argv);
+	os = new OS_JavaScript(argc, argv);
 	// TODO: Check error return value.
 	Main::setup(argv[0], argc - 1, &argv[1], false);
+	emscripten_set_main_loop(main_loop_callback, -1, false);
+	emscripten_pause_main_loop(); // Will need to wait for FS sync.
 
 	// Sync from persistent state into memory and then
 	// run the 'main_after_fs_sync' function.

+ 0 - 0
platform/javascript/http_request.js → platform/javascript/native/http_request.js


+ 1 - 1
platform/javascript/id_handler.js → platform/javascript/native/id_handler.js

@@ -28,7 +28,7 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
 
-var IDHandler = function() {
+var IDHandler = /** @constructor */ function() {
 
 	var ids = {};
 	var size = 0;

+ 204 - 0
platform/javascript/native/utils.js

@@ -0,0 +1,204 @@
+/*************************************************************************/
+/*  utils.js                                                             */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+Module['copyToFS'] = function(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 !== ERRNO_CODES.ENOENT) { // '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+'});
+}
+
+Module.drop_handler = (function() {
+	var upload = [];
+	var uploadPromises = [];
+	var uploadCallback = null;
+
+	function readFilePromise(entry, path) {
+		return new Promise(function(resolve, reject) {
+			entry.file(function(file) {
+				var reader = new FileReader();
+				reader.onload = function() {
+					var f = {
+						"path": file.relativePath || file.webkitRelativePath,
+						"name": file.name,
+						"type": file.type,
+						"size": file.size,
+						"data": reader.result
+					};
+					if (!f['path'])
+						f['path'] = f['name'];
+					upload.push(f);
+					resolve()
+				};
+				reader.onerror = function() {
+					console.log("Error reading file");
+					reject();
+				}
+
+				reader.readAsArrayBuffer(file);
+
+				}, function(err) {
+					console.log("Error!");
+					reject();
+				});
+		});
+	}
+
+	function readDirectoryPromise(entry) {
+		return new Promise(function(resolve, reject) {
+			var reader = entry.createReader();
+			reader.readEntries(function(entries) {
+				for (var i = 0; i < entries.length; i++) {
+					var ent = entries[i];
+					if (ent.isDirectory) {
+						uploadPromises.push(readDirectoryPromise(ent));
+					} else if (ent.isFile) {
+						uploadPromises.push(readFilePromise(ent));
+					}
+				}
+				resolve();
+			});
+		});
+	}
+
+	function processUploadsPromises(resolve, reject) {
+		if (uploadPromises.length == 0) {
+			resolve();
+			return;
+		}
+		uploadPromises.pop().then(function() {
+			setTimeout(function() {
+				processUploadsPromises(resolve, reject);
+				//processUploadsPromises.bind(null, resolve, reject)
+			}, 0);
+		});
+	}
+
+	function dropFiles(files) {
+		var args = files || [];
+		var argc = args.length;
+		var argv = stackAlloc((argc + 1) * 4);
+		for (var i = 0; i < argc; i++) {
+			HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i]);
+		}
+		HEAP32[(argv >> 2) + argc] = 0;
+		// Defined in javascript_main.cpp
+		ccall('_drop_files_callback', 'void', ['number', 'number'], [argv, argc]);
+	}
+
+	return function(ev) {
+		ev.preventDefault();
+		if (ev.dataTransfer.items) {
+			// Use DataTransferItemList interface to access the file(s)
+			for (var i = 0; i < ev.dataTransfer.items.length; i++) {
+				const item = ev.dataTransfer.items[i];
+				var entry = null;
+				if ("getAsEntry" in item) {
+					entry = item.getAsEntry();
+				} else if ("webkitGetAsEntry" in item) {
+					entry = item.webkitGetAsEntry();
+				}
+				if (!entry) {
+					console.error("File upload not supported");
+				} else if (entry.isDirectory) {
+					uploadPromises.push(readDirectoryPromise(entry));
+				} else if (entry.isFile) {
+					uploadPromises.push(readFilePromise(entry));
+				} else {
+					console.error("Unrecognized entry...", entry);
+				}
+			}
+		} else {
+			console.error("File upload not supported");
+		}
+		uploadCallback = new Promise(processUploadsPromises).then(function() {
+			const DROP = "/tmp/drop-" + parseInt(Math.random() * Math.pow(2, 31)) + "/";
+			var drops = [];
+			var files = [];
+			upload.forEach((elem) => {
+				var path = elem['path'];
+				Module['copyToFS'](DROP + path, elem['data']);
+				var idx = path.indexOf("/");
+				if (idx == -1) {
+					// Root file
+					drops.push(DROP + path);
+				} else {
+					// Subdir
+					var sub = path.substr(0, idx);
+					idx = sub.indexOf("/");
+					if (idx < 0 && drops.indexOf(DROP + sub) == -1) {
+						drops.push(DROP + sub);
+					}
+				}
+				files.push(DROP + path);
+			});
+			uploadPromises = [];
+			upload = [];
+			dropFiles(drops);
+			var dirs = [DROP.substr(0, DROP.length -1)];
+			files.forEach(function (file) {
+				FS.unlink(file);
+				var dir = file.replace(DROP, "");
+				var idx = dir.lastIndexOf("/");
+				while (idx > 0) {
+					dir = dir.substr(0, idx);
+					if (dirs.indexOf(DROP + dir) == -1) {
+						dirs.push(DROP + dir);
+					}
+					idx = dir.lastIndexOf("/");
+				}
+			});
+			// Remove dirs.
+			dirs = dirs.sort(function(a, b) {
+				var al = (a.match(/\//g) || []).length;
+				var bl = (b.match(/\//g) || []).length;
+				if (al > bl)
+					return -1;
+				else if (al < bl)
+					return 1;
+				return 0;
+			});
+			dirs.forEach(function(dir) {
+				FS.rmdir(dir);
+			});
+		});
+	}
+})();

+ 151 - 71
platform/javascript/os_javascript.cpp

@@ -31,12 +31,16 @@
 #include "os_javascript.h"
 
 #include "core/io/file_access_buffered_fa.h"
+#include "core/io/json.h"
 #include "drivers/gles2/rasterizer_gles2.h"
 #include "drivers/gles3/rasterizer_gles3.h"
 #include "drivers/unix/dir_access_unix.h"
 #include "drivers/unix/file_access_unix.h"
 #include "main/main.h"
 #include "servers/visual/visual_server_raster.h"
+#ifndef NO_THREADS
+#include "servers/visual/visual_server_wrap_mt.h"
+#endif
 
 #include <emscripten.h>
 #include <png.h>
@@ -49,42 +53,42 @@
 #define DOM_BUTTON_RIGHT 2
 #define DOM_BUTTON_XBUTTON1 3
 #define DOM_BUTTON_XBUTTON2 4
-#define GODOT_CANVAS_SELECTOR "#canvas"
 
 // Window (canvas)
 
 static void focus_canvas() {
 
 	/* clang-format off */
-	EM_ASM(
-		Module.canvas.focus();
-	);
+	EM_ASM({
+		Module['canvas'].focus();
+	});
 	/* clang-format on */
 }
 
 static bool is_canvas_focused() {
 
 	/* clang-format off */
-	return EM_ASM_INT_V(
-		return document.activeElement == Module.canvas;
-	);
+	return EM_ASM_INT({
+		return document.activeElement == Module['canvas'];
+	});
 	/* clang-format on */
 }
 
 static Point2 compute_position_in_canvas(int x, int y) {
+	OS_JavaScript *os = OS_JavaScript::get_singleton();
 	int canvas_x = EM_ASM_INT({
-		return document.getElementById('canvas').getBoundingClientRect().x;
+		return Module['canvas'].getBoundingClientRect().x;
 	});
 	int canvas_y = EM_ASM_INT({
-		return document.getElementById('canvas').getBoundingClientRect().y;
+		return Module['canvas'].getBoundingClientRect().y;
 	});
 	int canvas_width;
 	int canvas_height;
-	emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, &canvas_width, &canvas_height);
+	emscripten_get_canvas_element_size(os->canvas_id.utf8().get_data(), &canvas_width, &canvas_height);
 
 	double element_width;
 	double element_height;
-	emscripten_get_element_css_size(GODOT_CANVAS_SELECTOR, &element_width, &element_height);
+	emscripten_get_element_css_size(os->canvas_id.utf8().get_data(), &element_width, &element_height);
 
 	return Point2((int)(canvas_width / element_width * (x - canvas_x)),
 			(int)(canvas_height / element_height * (y - canvas_y)));
@@ -92,6 +96,16 @@ static Point2 compute_position_in_canvas(int x, int y) {
 
 static bool cursor_inside_canvas = true;
 
+extern "C" EMSCRIPTEN_KEEPALIVE void _canvas_resize_callback() {
+	OS_JavaScript *os = OS_JavaScript::get_singleton();
+	int canvas_width;
+	int canvas_height;
+	// Update the framebuffer size.
+	emscripten_get_canvas_element_size(os->canvas_id.utf8().get_data(), &canvas_width, &canvas_height);
+	emscripten_set_canvas_element_size(os->canvas_id.utf8().get_data(), canvas_width, canvas_height);
+	Main::force_redraw();
+}
+
 EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) {
 
 	OS_JavaScript *os = get_singleton();
@@ -141,14 +155,14 @@ void OS_JavaScript::set_window_size(const Size2 p_size) {
 			emscripten_exit_soft_fullscreen();
 			window_maximized = false;
 		}
-		emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, p_size.x, p_size.y);
+		emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), p_size.x, p_size.y);
 	}
 }
 
 Size2 OS_JavaScript::get_window_size() const {
 
 	int canvas[2];
-	emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, canvas, canvas + 1);
+	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);
 	return Size2(canvas[0], canvas[1]);
 }
 
@@ -168,7 +182,7 @@ void OS_JavaScript::set_window_maximized(bool p_enabled) {
 		strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
 		strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
 		strategy.canvasResizedCallback = NULL;
-		emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy);
+		emscripten_enter_soft_fullscreen(canvas_id.utf8().get_data(), &strategy);
 		window_maximized = p_enabled;
 	}
 }
@@ -197,7 +211,7 @@ void OS_JavaScript::set_window_fullscreen(bool p_enabled) {
 		strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
 		strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
 		strategy.canvasResizedCallback = NULL;
-		EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(GODOT_CANVAS_SELECTOR, false, &strategy);
+		EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(canvas_id.utf8().get_data(), false, &strategy);
 		ERR_FAIL_COND_MSG(result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED, "Enabling fullscreen is only possible from an input callback for the HTML5 platform.");
 		ERR_FAIL_COND_MSG(result != EMSCRIPTEN_RESULT_SUCCESS, "Enabling fullscreen is only possible from an input callback for the HTML5 platform.");
 		// Not fullscreen yet, so prevent "windowed" canvas dimensions from
@@ -434,8 +448,8 @@ static const char *godot2dom_cursor(OS::CursorShape p_shape) {
 static void set_css_cursor(const char *p_cursor) {
 
 	/* clang-format off */
-	EM_ASM_({
-		Module.canvas.style.cursor = UTF8ToString($0);
+	EM_ASM({
+		Module['canvas'].style.cursor = UTF8ToString($0);
 	}, p_cursor);
 	/* clang-format on */
 }
@@ -444,7 +458,7 @@ static bool is_css_cursor_hidden() {
 
 	/* clang-format off */
 	return EM_ASM_INT({
-		return Module.canvas.style.cursor === 'none';
+		return Module['canvas'].style.cursor === 'none';
 	});
 	/* clang-format on */
 }
@@ -697,9 +711,9 @@ EM_BOOL OS_JavaScript::wheel_callback(int p_event_type, const EmscriptenWheelEve
 bool OS_JavaScript::has_touchscreen_ui_hint() const {
 
 	/* clang-format off */
-	return EM_ASM_INT_V(
+	return EM_ASM_INT({
 		return 'ontouchstart' in window;
-	);
+	});
 	/* clang-format on */
 }
 
@@ -902,6 +916,7 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
 	emscripten_webgl_init_context_attributes(&attributes);
 	attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed");
 	attributes.antialias = false;
+	attributes.explicitSwapControl = true;
 	ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER);
 
 	if (p_desired.layered) {
@@ -945,8 +960,8 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
 		}
 	}
 
-	EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(GODOT_CANVAS_SELECTOR, &attributes);
-	if (emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS) {
+	webgl_ctx = emscripten_webgl_create_context(canvas_id.utf8().get_data(), &attributes);
+	if (emscripten_webgl_make_context_current(webgl_ctx) != EMSCRIPTEN_RESULT_SUCCESS) {
 		gl_initialization_error = true;
 	}
 
@@ -968,6 +983,7 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
 	if (p_desired.fullscreen) {
 		/* clang-format off */
 		EM_ASM({
+			const canvas = Module['canvas'];
 			(canvas.requestFullscreen || canvas.msRequestFullscreen ||
 				canvas.mozRequestFullScreen || canvas.mozRequestFullscreen ||
 				canvas.webkitRequestFullscreen
@@ -976,26 +992,22 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
 		/* clang-format on */
 	}
 	/* clang-format off */
-	if (EM_ASM_INT_V({ return Module.resizeCanvasOnStart })) {
+	if (EM_ASM_INT({ return Module['resizeCanvasOnStart'] })) {
 		/* clang-format on */
 		set_window_size(Size2(video_mode.width, video_mode.height));
 	} else {
 		set_window_size(get_window_size());
 	}
 
-	char locale_ptr[16];
-	/* clang-format off */
-	EM_ASM_ARGS({
-		stringToUTF8(Module.locale, $0, 16);
-	}, locale_ptr);
-	/* clang-format on */
-	setenv("LANG", locale_ptr, true);
-
 	AudioDriverManager::initialize(p_audio_driver);
-	VisualServer *visual_server = memnew(VisualServerRaster());
+	visual_server = memnew(VisualServerRaster());
+#ifndef NO_THREADS
+	visual_server = memnew(VisualServerWrapMT(visual_server, false));
+#endif
 	input = memnew(InputDefault);
 
 	EMSCRIPTEN_RESULT result;
+	CharString id = canvas_id.utf8().get_data();
 #define EM_CHECK(ev)                         \
 	if (result != EMSCRIPTEN_RESULT_SUCCESS) \
 	ERR_PRINTS("Error while setting " #ev " callback: Code " + itos(result))
@@ -1009,16 +1021,16 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
 	// JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM
 	// is used below.
 	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, mousedown, mouse_button_callback)
+	SET_EM_CALLBACK(id.get_data(), mousedown, mouse_button_callback)
 	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, wheel, wheel_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchstart, touch_press_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchmove, touchmove_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchend, touch_press_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, touchcancel, touch_press_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keydown, keydown_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keypress, keypress_callback)
-	SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, keyup, keyup_callback)
+	SET_EM_CALLBACK(id.get_data(), wheel, wheel_callback)
+	SET_EM_CALLBACK(id.get_data(), touchstart, touch_press_callback)
+	SET_EM_CALLBACK(id.get_data(), touchmove, touchmove_callback)
+	SET_EM_CALLBACK(id.get_data(), touchend, touch_press_callback)
+	SET_EM_CALLBACK(id.get_data(), touchcancel, touch_press_callback)
+	SET_EM_CALLBACK(id.get_data(), keydown, keydown_callback)
+	SET_EM_CALLBACK(id.get_data(), keypress, keypress_callback)
+	SET_EM_CALLBACK(id.get_data(), keyup, keyup_callback)
 	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback)
 	SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback)
 	SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback)
@@ -1027,22 +1039,45 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
 #undef EM_CHECK
 
 	/* clang-format off */
-	EM_ASM_ARGS({
+	EM_ASM({
+		Module.listeners = {};
+		const canvas = Module['canvas'];
 		const send_notification = cwrap('send_notification', null, ['number']);
 		const notifications = arguments;
 		(['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) {
-			Module.canvas.addEventListener(event, send_notification.bind(null, notifications[index]));
+			Module.listeners[event] = send_notification.bind(null, notifications[index]);
+			canvas.addEventListener(event, Module.listeners[event]);
 		});
 		// Clipboard
 		const update_clipboard = cwrap('update_clipboard', null, ['string']);
-		window.addEventListener('paste', function(evt) {
+		Module.listeners['paste'] = function(evt) {
 			update_clipboard(evt.clipboardData.getData('text'));
-		}, true);
+		};
+		window.addEventListener('paste', Module.listeners['paste'], true);
+		Module.listeners['dragover'] = function(ev) {
+			// Prevent default behavior (which would try to open the file(s))
+			ev.preventDefault();
+		};
+		// Drag an drop
+		Module.listeners['drop'] = Module.drop_handler; // Defined in native/utils.js
+		canvas.addEventListener('dragover', Module.listeners['dragover'], false);
+		canvas.addEventListener('drop', Module.listeners['drop'], false);
+		// Resize
+		const resize_callback = cwrap('_canvas_resize_callback', null, []);
+		Module.resize_observer = new window['ResizeObserver'](function(elements) {
+			resize_callback();
+		});
+		Module.resize_observer.observe(canvas);
+		// Quit request
+		Module['request_quit'] = function() {
+			send_notification(notifications[notifications.length - 1]);
+		};
 	},
 		MainLoop::NOTIFICATION_WM_MOUSE_ENTER,
 		MainLoop::NOTIFICATION_WM_MOUSE_EXIT,
 		MainLoop::NOTIFICATION_WM_FOCUS_IN,
-		MainLoop::NOTIFICATION_WM_FOCUS_OUT
+		MainLoop::NOTIFICATION_WM_FOCUS_OUT,
+		MainLoop::NOTIFICATION_WM_QUIT_REQUEST
 	);
 	/* clang-format on */
 
@@ -1051,6 +1086,10 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver,
 	return OK;
 }
 
+void OS_JavaScript::swap_buffers() {
+	emscripten_webgl_commit_frame();
+}
+
 void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) {
 
 	main_loop = p_main_loop;
@@ -1062,17 +1101,6 @@ MainLoop *OS_JavaScript::get_main_loop() const {
 	return main_loop;
 }
 
-void OS_JavaScript::run_async() {
-
-	main_loop->init();
-	emscripten_set_main_loop(main_loop_callback, -1, false);
-}
-
-void OS_JavaScript::main_loop_callback() {
-
-	get_singleton()->main_loop_iterate();
-}
-
 bool OS_JavaScript::main_loop_iterate() {
 
 	if (is_userfs_persistent() && sync_wait_time >= 0) {
@@ -1103,15 +1131,16 @@ bool OS_JavaScript::main_loop_iterate() {
 			strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
 			strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
 			strategy.canvasResizedCallback = NULL;
-			emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy);
+			emscripten_enter_soft_fullscreen(canvas_id.utf8().get_data(), &strategy);
 		} else {
-			emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, windowed_size.width, windowed_size.height);
+			emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), windowed_size.width, windowed_size.height);
 		}
+		emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), windowed_size.width, windowed_size.height);
 		just_exited_fullscreen = false;
 	}
 
 	int canvas[2];
-	emscripten_get_canvas_element_size(GODOT_CANVAS_SELECTOR, canvas, canvas + 1);
+	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);
 	video_mode.width = canvas[0];
 	video_mode.height = canvas[1];
 	if (!window_maximized && !video_mode.fullscreen && !just_exited_fullscreen && !entering_fullscreen) {
@@ -1127,16 +1156,54 @@ void OS_JavaScript::delete_main_loop() {
 	memdelete(main_loop);
 }
 
+void OS_JavaScript::finalize_async() {
+	EM_ASM({
+		const canvas = Module['canvas'];
+		Object.entries(Module.listeners).forEach(function(kv) {
+			if (kv[0] == 'paste') {
+				window.removeEventListener(kv[0], kv[1], true);
+			} else {
+				canvas.removeEventListener(kv[0], kv[1]);
+			}
+		});
+		Module.listeners = {};
+		Module.resize_observer.unobserve(canvas);
+		delete Module.resize_observer;
+	});
+	audio_driver_javascript.finish_async();
+}
+
 void OS_JavaScript::finalize() {
 
 	memdelete(input);
+	visual_server->finish();
+	emscripten_webgl_commit_frame();
+	memdelete(visual_server);
+	emscripten_webgl_destroy_context(webgl_ctx);
 }
 
 // Miscellaneous
 
 Error OS_JavaScript::execute(const String &p_path, const List<String> &p_arguments, bool p_blocking, ProcessID *r_child_id, String *r_pipe, int *r_exitcode, bool read_stderr, Mutex *p_pipe_mutex) {
 
-	ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "OS::execute() is not available on the HTML5 platform.");
+	Array args;
+	for (const List<String>::Element *E = p_arguments.front(); E; E = E->next()) {
+		args.push_back(E->get());
+	}
+	String json_args = JSON::print(args);
+	/* clang-format off */
+	int failed = EM_ASM_INT({
+		const json_args = UTF8ToString($0);
+		const args = JSON.parse(json_args);
+		if (Module["onExecute"]) {
+			Module["onExecute"](args);
+			return 0;
+		}
+		return 1;
+	}, json_args.utf8().get_data());
+	/* clang-format on */
+	ERR_FAIL_COND_V_MSG(failed, ERR_UNAVAILABLE, "OS::execute() must be implemented in Javascript via 'engine.setOnExecute' if required.");
+	return OK;
 }
 
 Error OS_JavaScript::kill(const ProcessID &p_pid) {
@@ -1154,7 +1221,9 @@ extern "C" EMSCRIPTEN_KEEPALIVE void send_notification(int p_notification) {
 	if (p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER || p_notification == MainLoop::NOTIFICATION_WM_MOUSE_EXIT) {
 		cursor_inside_canvas = p_notification == MainLoop::NOTIFICATION_WM_MOUSE_ENTER;
 	}
-	OS_JavaScript::get_singleton()->get_main_loop()->notification(p_notification);
+	MainLoop *loop = OS_JavaScript::get_singleton()->get_main_loop();
+	if (loop)
+		loop->notification(p_notification);
 }
 
 bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {
@@ -1173,7 +1242,7 @@ bool OS_JavaScript::_check_internal_feature_support(const String &p_feature) {
 void OS_JavaScript::alert(const String &p_alert, const String &p_title) {
 
 	/* clang-format off */
-	EM_ASM_({
+	EM_ASM({
 		window.alert(UTF8ToString($0));
 	}, p_alert.utf8().get_data());
 	/* clang-format on */
@@ -1182,7 +1251,7 @@ void OS_JavaScript::alert(const String &p_alert, const String &p_title) {
 void OS_JavaScript::set_window_title(const String &p_title) {
 
 	/* clang-format off */
-	EM_ASM_({
+	EM_ASM({
 		document.title = UTF8ToString($0);
 	}, p_title.utf8().get_data());
 	/* clang-format on */
@@ -1221,7 +1290,7 @@ void OS_JavaScript::set_icon(const Ref<Image> &p_icon) {
 
 	r = png.read();
 	/* clang-format off */
-	EM_ASM_ARGS({
+	EM_ASM({
 		var PNG_PTR = $0;
 		var PNG_LEN = $1;
 
@@ -1248,7 +1317,7 @@ Error OS_JavaScript::shell_open(String p_uri) {
 
 	// Open URI in a new tab, browser will deal with it by protocol.
 	/* clang-format off */
-	EM_ASM_({
+	EM_ASM({
 		window.open(UTF8ToString($0), '_blank');
 	}, p_uri.utf8().get_data());
 	/* clang-format on */
@@ -1270,26 +1339,36 @@ String OS_JavaScript::get_user_data_dir() const {
 	return "/userfs";
 };
 
-String OS_JavaScript::get_resource_dir() const {
+String OS_JavaScript::get_cache_path() const {
+
+	return "/home/web_user/.cache";
+}
+
+String OS_JavaScript::get_config_path() const {
+
+	return "/home/web_user/.config";
+}
+
+String OS_JavaScript::get_data_path() const {
 
-	return "/";
+	return "/home/web_user/.local/share";
 }
 
 OS::PowerState OS_JavaScript::get_power_state() {
 
-	WARN_PRINT("Power management is not supported for the HTML5 platform, defaulting to POWERSTATE_UNKNOWN");
+	WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to POWERSTATE_UNKNOWN");
 	return OS::POWERSTATE_UNKNOWN;
 }
 
 int OS_JavaScript::get_power_seconds_left() {
 
-	WARN_PRINT("Power management is not supported for the HTML5 platform, defaulting to -1");
+	WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to -1");
 	return -1;
 }
 
 int OS_JavaScript::get_power_percent_left() {
 
-	WARN_PRINT("Power management is not supported for the HTML5 platform, defaulting to -1");
+	WARN_PRINT_ONCE("Power management is not supported for the HTML5 platform, defaulting to -1");
 	return -1;
 }
 
@@ -1336,6 +1415,7 @@ OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) {
 	transparency_enabled = false;
 
 	main_loop = NULL;
+	visual_server = NULL;
 
 	idb_available = false;
 	sync_wait_time = -1;

+ 10 - 4
platform/javascript/os_javascript.h

@@ -48,6 +48,8 @@ class OS_JavaScript : public OS_Unix {
 	bool just_exited_fullscreen;
 	bool transparency_enabled;
 
+	EMSCRIPTEN_WEBGL_CONTEXT_HANDLE webgl_ctx;
+
 	InputDefault *input;
 	Ref<InputEventKey> deferred_key_event;
 	CursorShape cursor_shape;
@@ -62,6 +64,7 @@ class OS_JavaScript : public OS_Unix {
 	MainLoop *main_loop;
 	int video_driver_index;
 	AudioDriverJavaScript audio_driver_javascript;
+	VisualServer *visual_server;
 
 	bool idb_available;
 	int64_t sync_wait_time;
@@ -84,8 +87,6 @@ class OS_JavaScript : public OS_Unix {
 	static EM_BOOL gamepad_change_callback(int p_event_type, const EmscriptenGamepadEvent *p_event, void *p_user_data);
 	void process_joypads();
 
-	static void main_loop_callback();
-
 	static void file_access_close_callback(const String &p_file, int p_flags);
 
 protected:
@@ -102,9 +103,13 @@ protected:
 	virtual bool _check_internal_feature_support(const String &p_feature);
 
 public:
+	String canvas_id;
+	void finalize_async();
+
 	// Override return type to make writing static callbacks less tedious.
 	static OS_JavaScript *get_singleton();
 
+	virtual void swap_buffers();
 	virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0);
 	virtual VideoMode get_video_mode(int p_screen = 0) const;
 	virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const;
@@ -142,7 +147,6 @@ public:
 	virtual String get_clipboard() const;
 
 	virtual MainLoop *get_main_loop() const;
-	void run_async();
 	bool main_loop_iterate();
 
 	virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = NULL, String *r_pipe = NULL, int *r_exitcode = NULL, bool read_stderr = false, Mutex *p_pipe_mutex = NULL);
@@ -157,7 +161,9 @@ public:
 	virtual String get_name() const;
 	virtual bool can_draw() const;
 
-	virtual String get_resource_dir() const;
+	virtual String get_cache_path() const;
+	virtual String get_config_path() const;
+	virtual String get_data_path() const;
 	virtual String get_user_data_dir() const;
 
 	virtual OS::PowerState get_power_state();

+ 0 - 5
platform/javascript/pre.js

@@ -1,5 +0,0 @@
-var Engine = {
-	RuntimeEnvironment: function(Module, exposedLibs) {
-		// The above is concatenated with generated code, and acts as the start of
-		// a wrapper for said code. See engine.js for the other part of the
-		// wrapper.