From 21c9f37757d564009e9e400142b3b4c70af237d3 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Sun, 19 Jan 2020 11:44:19 +0100 Subject: [PATCH] [HTML5] Refactor JS, threads support, closures. - Refactored the Engine code, splitted across files. - Use MODULARIZE option to build emscripten code into it's own closure. - Optional closure compiler run for JS and generated code. - Enable lto support (saves ~2MiB in release). - Can now build with tools=yes (not much to see yet). - Dropped some deprecated code for older toolchains. - Add onExit, and onExecute JS function. - Add files drag and drop support. - Add support for low precessor usage mode (via offscreen render, swap). --- platform/javascript/SCsub | 54 ++- .../javascript/audio_driver_javascript.cpp | 142 +++--- platform/javascript/audio_driver_javascript.h | 2 + platform/javascript/detect.py | 124 +++--- platform/javascript/emscripten_helpers.py | 38 ++ platform/javascript/engine.js | 411 ------------------ platform/javascript/engine/engine.js | 245 +++++++++++ platform/javascript/engine/externs.js | 3 + platform/javascript/engine/preloader.js | 139 ++++++ platform/javascript/engine/utils.js | 51 +++ platform/javascript/export/export.cpp | 8 + platform/javascript/javascript_main.cpp | 79 +++- .../javascript/{ => native}/http_request.js | 0 .../javascript/{ => native}/id_handler.js | 2 +- platform/javascript/native/utils.js | 204 +++++++++ platform/javascript/os_javascript.cpp | 203 ++++++--- platform/javascript/os_javascript.h | 14 +- platform/javascript/pre.js | 5 - 18 files changed, 1097 insertions(+), 627 deletions(-) create mode 100644 platform/javascript/emscripten_helpers.py delete mode 100644 platform/javascript/engine.js create mode 100644 platform/javascript/engine/engine.js create mode 100644 platform/javascript/engine/externs.js create mode 100644 platform/javascript/engine/preloader.js create mode 100644 platform/javascript/engine/utils.js rename platform/javascript/{ => native}/http_request.js (100%) rename platform/javascript/{ => native}/id_handler.js (98%) create mode 100644 platform/javascript/native/utils.js delete mode 100644 platform/javascript/pre.js diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index fd8ea9fec0d..8225c0103c2 100644 --- a/platform/javascript/SCsub +++ b/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 module in js_modules: - env.Append(LINKFLAGS=["--pre-js", env.File(module).path]) -env.Depends(build, js_modules) +for js in js_pre: + env.Append(LINKFLAGS=["--pre-js", env.File(js).path]) +env.Depends(build, js_pre) -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") +engine = [ + "engine/preloader.js", + "engine/utils.js", + "engine/engine.js", +] +externs = [env.File("#platform/javascript/engine/externs.js")] +js_engine = env.CreateEngineFile("#bin/godot${PROGSUFFIX}.engine.js", engine, externs) +env.Depends(js_engine, externs) + +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, diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 70e0dbf0f76..620135e1e17 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/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(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; + var ref = Module.IDHandler.get($0); + const ctx = ref['context']; + const BUFFER_LENGTH = $1; + const CHANNEL_COUNT = $2; - _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 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; } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 229d04ea602..c23d46fd191 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/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(); diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index fabcc5f96b4..5b7bcbc97c4 100644 --- a/platform/javascript/detect.py +++ b/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"]) diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py new file mode 100644 index 00000000000..a55c9d3f48f --- /dev/null +++ b/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]) diff --git a/platform/javascript/engine.js b/platform/javascript/engine.js deleted file mode 100644 index 227accadb0e..00000000000 --- a/platform/javascript/engine.js +++ /dev/null @@ -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; - } - } -})(); diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js new file mode 100644 index 00000000000..d709422abb4 --- /dev/null +++ b/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; +})(); diff --git a/platform/javascript/engine/externs.js b/platform/javascript/engine/externs.js new file mode 100644 index 00000000000..1a94dd15ec6 --- /dev/null +++ b/platform/javascript/engine/externs.js @@ -0,0 +1,3 @@ +var Godot; +var WebAssembly = {}; +WebAssembly.instantiate = function(buffer, imports) {}; diff --git a/platform/javascript/engine/preloader.js b/platform/javascript/engine/preloader.js new file mode 100644 index 00000000000..17918eae382 --- /dev/null +++ b/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; + } +}; diff --git a/platform/javascript/engine/utils.js b/platform/javascript/engine/utils.js new file mode 100644 index 00000000000..0c97b38199f --- /dev/null +++ b/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; + } +}; diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index 7cc20de5ce8..1cbb7ab88a0 100644 --- a/platform/javascript/export/export.cpp +++ b/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 &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"); diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 0bafad0af01..bb889fcf7c0 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -34,27 +34,98 @@ #include +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 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. diff --git a/platform/javascript/http_request.js b/platform/javascript/native/http_request.js similarity index 100% rename from platform/javascript/http_request.js rename to platform/javascript/native/http_request.js diff --git a/platform/javascript/id_handler.js b/platform/javascript/native/id_handler.js similarity index 98% rename from platform/javascript/id_handler.js rename to platform/javascript/native/id_handler.js index 3851123ed14..67d29075b8f 100644 --- a/platform/javascript/id_handler.js +++ b/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; diff --git a/platform/javascript/native/utils.js b/platform/javascript/native/utils.js new file mode 100644 index 00000000000..78e7ec7666d --- /dev/null +++ b/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); + }); + }); + } +})(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 6f00ba6bd18..49987133285 100644 --- a/platform/javascript/os_javascript.cpp +++ b/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 #include @@ -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))); @@ -141,14 +145,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 +172,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 +201,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 +438,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 +448,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 +701,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 +906,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 +950,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 +973,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 +982,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 +1011,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 +1029,39 @@ 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); + // 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 +1070,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 +1085,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 +1115,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 +1140,51 @@ void OS_JavaScript::delete_main_loop() { memdelete(main_loop); } +void OS_JavaScript::finalize_async() { + EM_ASM({ + Object.entries(Module.listeners).forEach(function(kv) { + if (kv[0] == 'paste') { + window.removeEventListener(kv[0], kv[1], true); + } else { + Module['canvas'].removeEventListener(kv[0], kv[1]); + } + }); + Module.listeners = {}; + }); + 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 &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::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 +1202,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 +1223,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 +1232,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 +1271,7 @@ void OS_JavaScript::set_icon(const Ref &p_icon) { r = png.read(); /* clang-format off */ - EM_ASM_ARGS({ + EM_ASM({ var PNG_PTR = $0; var PNG_LEN = $1; @@ -1248,7 +1298,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 +1320,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 "/"; + 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 "/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 +1396,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; diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 65a18830edb..9fcda31c4d5 100644 --- a/platform/javascript/os_javascript.h +++ b/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 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 *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 &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(); diff --git a/platform/javascript/pre.js b/platform/javascript/pre.js deleted file mode 100644 index a870e676ea8..00000000000 --- a/platform/javascript/pre.js +++ /dev/null @@ -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.