diff options
Diffstat (limited to 'platform/javascript')
23 files changed, 817 insertions, 762 deletions
diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index 85a633442e..7239648937 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -1,44 +1,63 @@ #!/usr/bin/env python -Import('env') +Import("env") javascript_files = [ - 'audio_driver_javascript.cpp', - 'http_client_javascript.cpp', - 'javascript_eval.cpp', - 'javascript_main.cpp', - 'os_javascript.cpp', + "audio_driver_javascript.cpp", + "http_client_javascript.cpp", + "javascript_eval.cpp", + "javascript_main.cpp", + "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', + "http_request.js", ] for lib in js_libraries: - env.Append(LINKFLAGS=['--js-library', env.File(lib).path]) + env.Append(LINKFLAGS=["--js-library", env.File(lib).path]) env.Depends(build, js_libraries) js_modules = [ - 'id_handler.js', + "id_handler.js", ] for module in js_modules: - env.Append(LINKFLAGS=['--pre-js', env.File(module).path]) + env.Append(LINKFLAGS=["--pre-js", env.File(module).path]) env.Depends(build, js_modules) -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') - -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' -]) -env.Zip('#bin/godot', zip_files, ZIPROOT=zip_dir, ZIPSUFFIX='${PROGSUFFIX}${ZIPSUFFIX}', ZIPCOMSTR='Archving $SOURCES as $TARGET') +engine = [ + "engine/preloader.js", + "engine/loader.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") +out_files = [zip_dir.File("godot.js"), zip_dir.File("godot.wasm"), zip_dir.File("godot.html")] +in_files = [js_wrapped, build[1], "#misc/dist/html/full-size.html"] +if env["threads_enabled"]: + in_files.append(build[2]) + out_files.append(zip_dir.File("godot.worker.js")) + +zip_files = env.InstallAs(out_files, in_files) +env.Zip( + "#bin/godot", + zip_files, + ZIPROOT=zip_dir, + ZIPSUFFIX="${PROGSUFFIX}${ZIPSUFFIX}", + ZIPCOMSTR="Archving $SOURCES as $TARGET", +) diff --git a/platform/javascript/api/api.cpp b/platform/javascript/api/api.cpp index 88de13d771..45cb82b351 100644 --- a/platform/javascript/api/api.cpp +++ b/platform/javascript/api/api.cpp @@ -46,7 +46,7 @@ void unregister_javascript_api() { memdelete(javascript_eval); } -JavaScript *JavaScript::singleton = NULL; +JavaScript *JavaScript::singleton = nullptr; JavaScript *JavaScript::get_singleton() { @@ -55,7 +55,7 @@ JavaScript *JavaScript::get_singleton() { JavaScript::JavaScript() { - ERR_FAIL_COND_MSG(singleton != NULL, "JavaScript singleton already exist."); + ERR_FAIL_COND_MSG(singleton != nullptr, "JavaScript singleton already exist."); singleton = this; } diff --git a/platform/javascript/api/api.h b/platform/javascript/api/api.h index 164d679205..8afe0f33ce 100644 --- a/platform/javascript/api/api.h +++ b/platform/javascript/api/api.h @@ -28,5 +28,10 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#ifndef JAVASCRIPT_API_H +#define JAVASCRIPT_API_H + void register_javascript_api(); void unregister_javascript_api(); + +#endif // JAVASCRIPT_API_H diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index f1bc7c4382..8f857478e5 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -32,7 +32,7 @@ #include <emscripten.h> -AudioDriverJavaScript *AudioDriverJavaScript::singleton = NULL; +AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr; const char *AudioDriverJavaScript::get_name() const { @@ -69,31 +69,37 @@ void AudioDriverJavaScript::process_capture(float sample) { Error AudioDriverJavaScript::init() { /* clang-format off */ - EM_ASM({ - _audioDriver_audioContext = new (window.AudioContext || window.webkitAudioContext); - _audioDriver_audioInput = null; - _audioDriver_inputStream = null; - _audioDriver_scriptNode = null; + _driver_id = EM_ASM_INT({ + return Module.IDHandler.add({ + 'context': new (window.AudioContext || window.webkitAudioContext), + 'input': null, + 'stream': null, + 'script': null + }); }); /* clang-format on */ int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); /* clang-format off */ buffer_length = EM_ASM_INT({ - var CHANNEL_COUNT = $0; + var ref = Module.IDHandler.get($0); + var ctx = ref['context']; + var CHANNEL_COUNT = $1; - var channelCount = _audioDriver_audioContext.destination.channelCount; + var channelCount = ctx.destination.channelCount; + var script = null; try { // Try letting the browser recommend a buffer length. - _audioDriver_scriptNode = _audioDriver_audioContext.createScriptProcessor(0, 2, channelCount); + script = ctx.createScriptProcessor(0, 2, channelCount); } catch (e) { // ...otherwise, default to 4096. - _audioDriver_scriptNode = _audioDriver_audioContext.createScriptProcessor(4096, 2, channelCount); + script = ctx.createScriptProcessor(4096, 2, channelCount); } - _audioDriver_scriptNode.connect(_audioDriver_audioContext.destination); + script.connect(ctx.destination); + ref['script'] = script; - return _audioDriver_scriptNode.bufferSize; - }, channel_count); + return script.bufferSize; + }, _driver_id, channel_count); /* clang-format on */ if (!buffer_length) { return FAILED; @@ -112,11 +118,12 @@ 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; @@ -133,7 +140,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++) { @@ -142,34 +149,37 @@ 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 */ } 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 */ } @@ -184,16 +194,15 @@ void AudioDriverJavaScript::finish() { /* clang-format off */ EM_ASM({ - _audioDriver_audioContext = null; - _audioDriver_audioInput = null; - _audioDriver_scriptNode = null; - }); + Module.IDHandler.remove($0); + }, _driver_id); /* clang-format on */ if (internal_buffer) { memdelete_arr(internal_buffer); - internal_buffer = NULL; + internal_buffer = nullptr; } + _driver_id = 0; } Error AudioDriverJavaScript::capture_start() { @@ -203,9 +212,10 @@ Error AudioDriverJavaScript::capture_start() { /* 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) { @@ -219,7 +229,7 @@ Error AudioDriverJavaScript::capture_start() { navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); } - }); + }, _driver_id); /* clang-format on */ return OK; @@ -229,20 +239,21 @@ 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(); @@ -252,7 +263,9 @@ Error AudioDriverJavaScript::capture_stop() { AudioDriverJavaScript::AudioDriverJavaScript() { - internal_buffer = NULL; + _driver_id = 0; + internal_buffer = nullptr; + buffer_length = 0; singleton = this; } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 2bb97ba192..f6f2dacd4e 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: diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 1766833364..9486e10717 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -1,35 +1,40 @@ import os +from emscripten_helpers import parse_config, run_closure_compiler, create_engine_file + def is_active(): return True def get_name(): - return 'JavaScript' + return "JavaScript" def can_build(): - return 'EM_CONFIG' in os.environ or os.path.exists(os.path.expanduser('~/.emscripten')) + return "EM_CONFIG" in os.environ or os.path.exists(os.path.expanduser("~/.emscripten")) def get_opts(): from SCons.Variables import BoolVariable + return [ # eval() can be a security concern, so it can be disabled. - BoolVariable('javascript_eval', 'Enable JavaScript eval interface', True), + 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), ] def get_flags(): return [ - ('tools', False), - ('builtin_pcre2_with_jit', False), + ("tools", False), + ("builtin_pcre2_with_jit", False), # Disabling the mbedtls module reduces file size. # The module has little use due to the limited networking functionality # in this platform. For the available networking methods, the browser # manages TLS. - ('module_mbedtls_enabled', False), + ("module_mbedtls_enabled", False), ] @@ -37,124 +42,125 @@ 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. # -Os reduces file size by around 5 MiB over -O3. -Oz only saves about # 100 KiB over -Os, which does not justify the negative impact on # 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: - env.Append(CPPDEFINES=['DEBUG_ENABLED']) - env.Append(CCFLAGS=['-O1', '-g']) - env.Append(LINKFLAGS=['-O1', '-g']) - env.Append(LINKFLAGS=['-s', 'ASSERTIONS=1']) - - ## Compiler configuration - - 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')) + env.Append(CCFLAGS=["-Os"]) + env.Append(LINKFLAGS=["-Os"]) + 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"]) + + 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: - raise RuntimeError("'BINARYEN_ROOT' or 'EMSCRIPTEN_ROOT' missing in Emscripten configuration file '%s'" % em_config_file) + # 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"]) - env['CC'] = 'emcc' - env['CXX'] = 'em++' - env['LINK'] = 'emcc' + ## Copy env variables. + env["ENV"] = os.environ - env['AR'] = 'emar' - env['RANLIB'] = 'emranlib' + # 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"]) - # Use TempFileMunge since some AR invocations are too long for cmd.exe. - # Use POSIX-style paths, required with TempFileMunge. - env['ARCOM_POSIX'] = env['ARCOM'].replace( - '$TARGET', '$TARGET.posix').replace( - '$SOURCES', '$SOURCES.posix') - env['ARCOM'] = '${TEMPFILE(ARCOM_POSIX)}' + # 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}) - # All intermediate files are just LLVM bitcode. - env['OBJPREFIX'] = '' - env['OBJSUFFIX'] = '.bc' - env['PROGPREFIX'] = '' - # Program() output consists of multiple files, so specify suffixes manually at builder. - env['PROGSUFFIX'] = '' - env['LIBPREFIX'] = 'lib' - env['LIBSUFFIX'] = '.bc' - env['LIBPREFIXES'] = ['$LIBPREFIX'] - env['LIBSUFFIXES'] = ['$LIBSUFFIX'] + # Add method that joins/compiles our Engine files. + env.AddMethod(create_engine_file, "CreateEngineFile") - ## Compile flags + # Closure compiler extern and support for ecmascript specs (const, let, etc). + env["ENV"]["EMCC_CLOSURE_ARGS"] = "--language_in ECMASCRIPT6" - env.Prepend(CPPPATH=['#platform/javascript']) - env.Append(CPPDEFINES=['JAVASCRIPT_ENABLED', 'UNIX_ENABLED']) + em_config = parse_config() + env.PrependENVPath("PATH", em_config["EMCC_ROOT"]) - # No multi-threading (SharedArrayBuffer) available yet, - # once feasible also consider memory buffer size issues. - env.Append(CPPDEFINES=['NO_THREADS']) + env["CC"] = "emcc" + env["CXX"] = "em++" + env["LINK"] = "emcc" - # 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']) + env["AR"] = "emar" + env["RANLIB"] = "emranlib" - if env['javascript_eval']: - env.Append(CPPDEFINES=['JAVASCRIPT_EVAL_ENABLED']) + # Use TempFileMunge since some AR invocations are too long for cmd.exe. + # Use POSIX-style paths, required with TempFileMunge. + env["ARCOM_POSIX"] = env["ARCOM"].replace("$TARGET", "$TARGET.posix").replace("$SOURCES", "$SOURCES.posix") + env["ARCOM"] = "${TEMPFILE(ARCOM_POSIX)}" + + # All intermediate files are just LLVM bitcode. + env["OBJPREFIX"] = "" + env["OBJSUFFIX"] = ".bc" + env["PROGPREFIX"] = "" + # Program() output consists of multiple files, so specify suffixes manually at builder. + env["PROGSUFFIX"] = "" + env["LIBPREFIX"] = "lib" + env["LIBSUFFIX"] = ".bc" + env["LIBPREFIXES"] = ["$LIBPREFIX"] + env["LIBSUFFIXES"] = ["$LIBSUFFIX"] + + env.Prepend(CPPPATH=["#platform/javascript"]) + env.Append(CPPDEFINES=["JAVASCRIPT_ENABLED", "UNIX_ENABLED"]) + + if env["javascript_eval"]: + env.Append(CPPDEFINES=["JAVASCRIPT_EVAL_ENABLED"]) + + # 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"]) - ## Link flags + # 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']) + env.Append(LIBS=["idbfs.js"]) - # 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", "BINARYEN=1"]) + 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 # us since we don't know requirements at compile-time. - env.Append(LINKFLAGS=['-s', 'ALLOW_MEMORY_GROWTH=1']) + env.Append(LINKFLAGS=["-s", "ALLOW_MEMORY_GROWTH=1"]) # This setting just makes WebGL 2 APIs available, it does NOT disable WebGL 1. - env.Append(LINKFLAGS=['-s', 'USE_WEBGL2=1']) - - env.Append(LINKFLAGS=['-s', 'INVOKE_RUN=0']) + env.Append(LINKFLAGS=["-s", "USE_WEBGL2=1"]) - # TODO: Reevaluate usage of this setting now that engine.js manages engine runtime. - env.Append(LINKFLAGS=['-s', 'NO_EXIT_RUNTIME=1']) + env.Append(LINKFLAGS=["-s", "INVOKE_RUN=0"]) - #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. + env.Append(LINKFLAGS=["-s", 'EXTRA_EXPORTED_RUNTIME_METHODS=["callMain", "FS"]']) diff --git a/platform/javascript/dom_keys.inc b/platform/javascript/dom_keys.inc index 25e88f99d1..fd9df765d2 100644 --- a/platform/javascript/dom_keys.inc +++ b/platform/javascript/dom_keys.inc @@ -218,7 +218,7 @@ #define DOM_VK_PA1 0xFD #define DOM_VK_WIN_OEM_CLEAR 0xFE -int dom2godot_scancode(int dom_keycode) { +int dom2godot_keycode(int dom_keycode) { if (DOM_VK_0 <= dom_keycode && dom_keycode <= DOM_VK_Z) { // ASCII intersection diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py new file mode 100644 index 0000000000..a55c9d3f48 --- /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 1f78aa672d..0000000000 --- a/platform/javascript/engine.js +++ /dev/null @@ -1,413 +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', mainPack ]; - - return Promise.all([ - // Load from directory, - this.init(getBasePath(mainPack)), - // ...but write to root where the engine expects it. - 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 0000000000..6d7509377f --- /dev/null +++ b/platform/javascript/engine/engine.js @@ -0,0 +1,184 @@ +Function('return this')()['Engine'] = (function() { + + var unloadAfterInit = true; + var canvas = null; + var resizeCanvasOnStart = false; + var customLocale = 'en_US'; + var wasmExt = '.wasm'; + + var preloader = new Preloader(); + var loader = new Loader(); + var rtenv = null; + + var executableName = ''; + 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() {}; + + Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { + if (initPromise) { + return initPromise; + } + if (!loadPromise) { + 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; + initPromise = loader.init(loadPromise, loadPath, config).then(function() { + return new Promise(function(resolve, reject) { + rtenv = loader.env; + if (unloadAfterInit) { + loadPromise = null; + } + resolve(); + }); + }); + 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 new Promise(function(resolve, reject) { + return me.init().then(function() { + if (!(canvas instanceof HTMLCanvasElement)) { + canvas = Utils.findCanvas(); + } + rtenv['locale'] = customLocale; + rtenv['canvas'] = canvas; + rtenv['thisProgram'] = executableName; + rtenv['resizeCanvasOnStart'] = resizeCanvasOnStart; + loader.start(preloader.preloadedFiles, args).then(function() { + loader = null; + initPromise = null; + resolve(); + }); + }); + }); + }; + + Engine.prototype.startGame = function(execName, mainPack) { + // Start and init with execName as loadPath if not inited. + executableName = execName; + var me = this; + return Promise.all([ + this.init(execName), + this.preloadFile(mainPack, mainPack) + ]).then(function() { + return me.start('--main-pack', mainPack); + }); + }; + + 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) { + canvas = canvasElem; + }; + + Engine.prototype.setCanvasResizedOnStart = function(enabled) { + resizeCanvasOnStart = enabled; + }; + + Engine.prototype.setLocale = function(locale) { + customLocale = locale; + }; + + Engine.prototype.setExecutableName = function(newName) { + 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 (rtenv) + 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 (rtenv) + rtenv.printErr = printErr; + stderr = printErr; + }; + + // 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 + return Engine; +})(); diff --git a/platform/javascript/engine/externs.js b/platform/javascript/engine/externs.js new file mode 100644 index 0000000000..1a94dd15ec --- /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/loader.js b/platform/javascript/engine/loader.js new file mode 100644 index 0000000000..d27fbf612e --- /dev/null +++ b/platform/javascript/engine/loader.js @@ -0,0 +1,33 @@ +var Loader = /** @constructor */ function() { + + this.env = null; + + this.init = function(loadPromise, basePath, config) { + var me = this; + return new Promise(function(resolve, reject) { + var cfg = config || {}; + cfg['locateFile'] = Utils.createLocateRewrite(basePath); + cfg['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); + loadPromise = null; + Godot(cfg).then(function(module) { + me.env = module; + resolve(); + }); + }); + } + + this.start = function(preloadedFiles, args) { + var me = this; + return new Promise(function(resolve, reject) { + if (!me.env) { + reject(new Error('The engine must be initialized before it can be started')); + } + preloadedFiles.forEach(function(file) { + Utils.copyToFS(me.env['FS'], file.path, file.buffer); + }); + preloadedFiles.length = 0; // Clear memory + me.env['callMain'](args); + resolve(); + }); + } +}; diff --git a/platform/javascript/engine/preloader.js b/platform/javascript/engine/preloader.js new file mode 100644 index 0000000000..17918eae38 --- /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 0000000000..fdff90a923 --- /dev/null +++ b/platform/javascript/engine/utils.js @@ -0,0 +1,69 @@ +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; + }, + + copyToFS: function(fs, 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 !== 44) { // '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+'}); + }, + + 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 c1cb8bcb58..36076a2af9 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"; @@ -200,7 +203,7 @@ class EditorExportPlatformJavaScript : public EditorExportPlatform { private: Ref<EditorHTTPServer> server; bool server_quit; - Mutex *server_lock; + Mutex server_lock; Thread *server_thread; static void _server_thread_poll(void *data); @@ -212,7 +215,7 @@ public: virtual String get_name() const; virtual String get_os_name() const; - virtual Ref<Texture> get_logo() const; + virtual Ref<Texture2D> get_logo() const; virtual bool can_export(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates) const; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const; @@ -224,7 +227,7 @@ public: virtual String get_option_tooltip(int p_index) const { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); } virtual Ref<ImageTexture> get_option_icon(int p_index) const; virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags); - virtual Ref<Texture> get_run_icon() const; + virtual Ref<Texture2D> get_run_icon() const; virtual void get_platform_features(List<String> *r_features) { @@ -249,6 +252,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re String current_line = lines[i]; current_line = current_line.replace("$GODOT_BASENAME", p_name); + current_line = current_line.replace("$GODOT_PROJECT_NAME", ProjectSettings::get_singleton()->get_setting("application/config/name")); current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include")); current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false"); str_export += current_line + "\n"; @@ -271,11 +275,9 @@ void EditorExportPlatformJavaScript::get_preset_features(const Ref<EditorExportP String driver = ProjectSettings::get_singleton()->get("rendering/quality/driver/driver_name"); if (driver == "GLES2") { r_features->push_back("etc"); - } else if (driver == "GLES3") { + } else if (driver == "Vulkan") { + // FIXME: Review if this is correct. r_features->push_back("etc2"); - if (ProjectSettings::get_singleton()->get("rendering/quality/driver/fallback_to_gles2")) { - r_features->push_back("etc"); - } } } } @@ -300,7 +302,7 @@ String EditorExportPlatformJavaScript::get_os_name() const { return "HTML5"; } -Ref<Texture> EditorExportPlatformJavaScript::get_logo() const { +Ref<Texture2D> EditorExportPlatformJavaScript::get_logo() const { return logo; } @@ -389,7 +391,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese return error; } - FileAccess *src_f = NULL; + FileAccess *src_f = nullptr; zlib_filefunc_def io = zipio_create_io_from_file(&src_f); unzFile pkg = unzOpen2(template_path.utf8().get_data(), &io); @@ -409,7 +411,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese //get filename unz_file_info info; char fname[16384]; - unzGetCurrentFileInfo(pkg, &info, fname, 16384, NULL, 0, NULL, 0); + unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0); String file = fname; @@ -434,6 +436,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"; @@ -533,9 +539,8 @@ bool EditorExportPlatformJavaScript::poll_export() { menu_options = preset.is_valid(); if (server->is_listening()) { if (menu_options == 0) { - server_lock->lock(); + MutexLock lock(server_lock); server->stop(); - server_lock->unlock(); } else { menu_options += 1; } @@ -555,9 +560,8 @@ int EditorExportPlatformJavaScript::get_options_count() const { Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) { if (p_option == 1) { - server_lock->lock(); + MutexLock lock(server_lock); server->stop(); - server_lock->unlock(); return OK; } @@ -567,6 +571,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"); @@ -586,10 +591,12 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'."); // Restart server. - server_lock->lock(); - server->stop(); - err = server->listen(bind_port, bind_ip); - server_lock->unlock(); + { + MutexLock lock(server_lock); + + server->stop(); + err = server->listen(bind_port, bind_ip); + } ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to start HTTP server."); OS::get_singleton()->shell_open(String("http://" + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html")); @@ -598,7 +605,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese return OK; } -Ref<Texture> EditorExportPlatformJavaScript::get_run_icon() const { +Ref<Texture2D> EditorExportPlatformJavaScript::get_run_icon() const { return run_icon; } @@ -607,9 +614,10 @@ void EditorExportPlatformJavaScript::_server_thread_poll(void *data) { EditorExportPlatformJavaScript *ej = (EditorExportPlatformJavaScript *)data; while (!ej->server_quit) { OS::get_singleton()->delay_usec(1000); - ej->server_lock->lock(); - ej->server->poll(); - ej->server_lock->unlock(); + { + MutexLock lock(ej->server_lock); + ej->server->poll(); + } } } @@ -617,7 +625,6 @@ EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() { server.instance(); server_quit = false; - server_lock = Mutex::create(); server_thread = Thread::create(_server_thread_poll, this); Ref<Image> img = memnew(Image(_javascript_logo)); @@ -641,7 +648,6 @@ EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() { server->stop(); server_quit = true; Thread::wait_to_finish(server_thread); - memdelete(server_lock); memdelete(server_thread); } diff --git a/platform/javascript/http_client.h.inc b/platform/javascript/http_client.h.inc index 03e2ce8b8a..ac275aadbc 100644 --- a/platform/javascript/http_client.h.inc +++ b/platform/javascript/http_client.h.inc @@ -45,7 +45,7 @@ String password; int polled_response_code; String polled_response_header; -PoolByteArray polled_response; +PackedByteArray polled_response; #ifdef DEBUG_ENABLED bool has_polled; diff --git a/platform/javascript/http_client_javascript.cpp b/platform/javascript/http_client_javascript.cpp index 2c2511a3a5..863c207896 100644 --- a/platform/javascript/http_client_javascript.cpp +++ b/platform/javascript/http_client_javascript.cpp @@ -88,8 +88,8 @@ Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Ve String url = (use_tls ? "https://" : "http://") + host + ":" + itos(port) + p_url; godot_xhr_reset(xhr_id); godot_xhr_open(xhr_id, _methods[p_method], url.utf8().get_data(), - username.empty() ? NULL : username.utf8().get_data(), - password.empty() ? NULL : password.utf8().get_data()); + username.empty() ? nullptr : username.utf8().get_data(), + password.empty() ? nullptr : password.utf8().get_data()); for (int i = 0; i < p_headers.size(); i++) { int header_separator = p_headers[i].find(": "); @@ -103,13 +103,12 @@ Error HTTPClient::prepare_request(Method p_method, const String &p_url, const Ve return OK; } -Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector<String> &p_headers, const PoolVector<uint8_t> &p_body) { +Error HTTPClient::request_raw(Method p_method, const String &p_url, const Vector<String> &p_headers, const Vector<uint8_t> &p_body) { Error err = prepare_request(p_method, p_url, p_headers); if (err != OK) return err; - PoolByteArray::Read read = p_body.read(); - godot_xhr_send_data(xhr_id, read.ptr(), p_body.size()); + godot_xhr_send_data(xhr_id, p_body.ptr(), p_body.size()); return OK; } @@ -173,18 +172,14 @@ int HTTPClient::get_response_body_length() const { return polled_response.size(); } -PoolByteArray HTTPClient::read_response_body_chunk() { +PackedByteArray HTTPClient::read_response_body_chunk() { - ERR_FAIL_COND_V(status != STATUS_BODY, PoolByteArray()); + ERR_FAIL_COND_V(status != STATUS_BODY, PackedByteArray()); int to_read = MIN(read_limit, polled_response.size() - response_read_offset); - PoolByteArray chunk; + PackedByteArray chunk; chunk.resize(to_read); - PoolByteArray::Write write = chunk.write(); - PoolByteArray::Read read = polled_response.read(); - memcpy(write.ptr(), read.ptr() + response_read_offset, to_read); - write = PoolByteArray::Write(); - read = PoolByteArray::Read(); + memcpy(chunk.ptrw(), polled_response.ptr() + response_read_offset, to_read); response_read_offset += to_read; if (response_read_offset == polled_response.size()) { @@ -263,23 +258,17 @@ Error HTTPClient::poll() { status = STATUS_BODY; - PoolByteArray bytes; + PackedByteArray bytes; int len = godot_xhr_get_response_headers_length(xhr_id); bytes.resize(len + 1); - PoolByteArray::Write write = bytes.write(); - godot_xhr_get_response_headers(xhr_id, reinterpret_cast<char *>(write.ptr()), len); - write[len] = 0; - write = PoolByteArray::Write(); + godot_xhr_get_response_headers(xhr_id, reinterpret_cast<char *>(bytes.ptrw()), len); + bytes.ptrw()[len] = 0; - PoolByteArray::Read read = bytes.read(); - polled_response_header = String::utf8(reinterpret_cast<const char *>(read.ptr())); - read = PoolByteArray::Read(); + polled_response_header = String::utf8(reinterpret_cast<const char *>(bytes.ptr())); polled_response.resize(godot_xhr_get_response_length(xhr_id)); - write = polled_response.write(); - godot_xhr_get_response(xhr_id, write.ptr(), polled_response.size()); - write = PoolByteArray::Write(); + godot_xhr_get_response(xhr_id, polled_response.ptrw(), polled_response.size()); break; } diff --git a/platform/javascript/http_request.h b/platform/javascript/http_request.h index 57dc4f0d9f..54e98c1927 100644 --- a/platform/javascript/http_request.h +++ b/platform/javascript/http_request.h @@ -49,7 +49,7 @@ extern int godot_xhr_new(); extern void godot_xhr_reset(int p_xhr_id); extern bool godot_xhr_free(int p_xhr_id); -extern int godot_xhr_open(int p_xhr_id, const char *p_method, const char *p_url, const char *p_user = NULL, const char *p_password = NULL); +extern int godot_xhr_open(int p_xhr_id, const char *p_method, const char *p_url, const char *p_user = nullptr, const char *p_password = nullptr); extern void godot_xhr_set_request_header(int p_xhr_id, const char *p_header, const char *p_value); diff --git a/platform/javascript/id_handler.js b/platform/javascript/id_handler.js index 3851123ed1..67d29075b8 100644 --- a/platform/javascript/id_handler.js +++ b/platform/javascript/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/javascript_eval.cpp b/platform/javascript/javascript_eval.cpp index d907222d24..db8050b90e 100644 --- a/platform/javascript/javascript_eval.cpp +++ b/platform/javascript/javascript_eval.cpp @@ -33,11 +33,11 @@ #include "api/javascript_eval.h" #include "emscripten.h" -extern "C" EMSCRIPTEN_KEEPALIVE uint8_t *resize_poolbytearray_and_open_write(PoolByteArray *p_arr, PoolByteArray::Write *r_write, int p_len) { +extern "C" EMSCRIPTEN_KEEPALIVE uint8_t *resize_PackedByteArray_and_open_write(PackedByteArray *p_arr, VectorWriteProxy<uint8_t> *r_write, int p_len) { p_arr->resize(p_len); - *r_write = p_arr->write(); - return r_write->ptr(); + *r_write = p_arr->write; + return p_arr->ptrw(); } Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { @@ -48,8 +48,8 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { char *s; } js_data; - PoolByteArray arr; - PoolByteArray::Write arr_write; + PackedByteArray arr; + VectorWriteProxy<uint8_t> arr_write; /* clang-format off */ Variant::Type return_type = static_cast<Variant::Type>(EM_ASM_INT({ @@ -81,7 +81,7 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { case 'number': setValue(PTR, eval_ret, 'double'); - return 3; // REAL + return 3; // FLOAT case 'string': var array_len = lengthBytesUTF8(eval_ret)+1; @@ -114,9 +114,9 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { eval_ret = new Uint8Array(eval_ret); } if (eval_ret instanceof Uint8Array) { - var bytes_ptr = ccall('resize_poolbytearray_and_open_write', 'number', ['number', 'number' ,'number'], [BYTEARRAY_PTR, BYTEARRAY_WRITE_PTR, eval_ret.length]); + var bytes_ptr = ccall('resize_PackedByteArray_and_open_write', 'number', ['number', 'number' ,'number'], [BYTEARRAY_PTR, BYTEARRAY_WRITE_PTR, eval_ret.length]); HEAPU8.set(eval_ret, bytes_ptr); - return 20; // POOL_BYTE_ARRAY + return 20; // PACKED_BYTE_ARRAY } break; } @@ -128,7 +128,7 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { switch (return_type) { case Variant::BOOL: return js_data.b; - case Variant::REAL: + case Variant::FLOAT: return js_data.d; case Variant::STRING: { String str = String::utf8(js_data.s); @@ -137,8 +137,8 @@ Variant JavaScript::eval(const String &p_code, bool p_use_global_exec_context) { /* clang-format on */ return str; } - case Variant::POOL_BYTE_ARRAY: - arr_write = PoolByteArray::Write(); + case Variant::PACKED_BYTE_ARRAY: + arr_write = VectorWriteProxy<uint8_t>(); return arr; default: return Variant(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 34dce90b6b..ad06aef86e 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -31,12 +31,12 @@ #include "os_javascript.h" #include "core/io/file_access_buffered_fa.h" -#include "drivers/gles2/rasterizer_gles2.h" -#include "drivers/gles3/rasterizer_gles3.h" +//#include "drivers/gles2/rasterizer_gles2.h" +#include "drivers/dummy/rasterizer_dummy.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" +#include "servers/rendering/rendering_server_raster.h" #include <emscripten.h> #include <png.h> @@ -167,7 +167,7 @@ void OS_JavaScript::set_window_maximized(bool p_enabled) { strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = NULL; + strategy.canvasResizedCallback = nullptr; emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy); window_maximized = p_enabled; } @@ -196,7 +196,7 @@ void OS_JavaScript::set_window_fullscreen(bool p_enabled) { strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = NULL; + strategy.canvasResizedCallback = nullptr; EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(GODOT_CANVAS_SELECTOR, 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."); @@ -220,6 +220,20 @@ void OS_JavaScript::get_fullscreen_mode_list(List<VideoMode> *p_list, int p_scre p_list->push_back(OS::VideoMode(screen.width, screen.height, true)); } +bool OS_JavaScript::get_window_per_pixel_transparency_enabled() const { + if (!is_layered_allowed()) { + return false; + } + return transparency_enabled; +} + +void OS_JavaScript::set_window_per_pixel_transparency_enabled(bool p_enabled) { + if (!is_layered_allowed()) { + return; + } + transparency_enabled = p_enabled; +} + // Keys template <typename T> @@ -237,7 +251,8 @@ static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscrip ev.instance(); ev->set_echo(emscripten_event->repeat); dom2godot_mod(emscripten_event, ev); - ev->set_scancode(dom2godot_scancode(emscripten_event->keyCode)); + ev->set_keycode(dom2godot_keycode(emscripten_event->keyCode)); + ev->set_physical_keycode(dom2godot_keycode(emscripten_event->keyCode)); String unicode = String::utf8(emscripten_event->key); // Check if empty or multi-character (e.g. `CapsLock`). @@ -257,7 +272,7 @@ EM_BOOL OS_JavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboa OS_JavaScript *os = get_singleton(); Ref<InputEventKey> ev = setup_key_event(p_event); ev->set_pressed(true); - if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_scancode())) { + if (ev->get_unicode() == 0 && keycode_has_unicode(ev->get_keycode())) { // Defer to keypress event for legacy unicode retrieval. os->deferred_key_event = ev; // Do not suppress keypress event. @@ -282,7 +297,7 @@ EM_BOOL OS_JavaScript::keyup_callback(int p_event_type, const EmscriptenKeyboard Ref<InputEventKey> ev = setup_key_event(p_event); ev->set_pressed(false); get_singleton()->input->parse_input_event(ev); - return ev->get_scancode() != KEY_UNKNOWN && ev->get_scancode() != 0; + return ev->get_keycode() != KEY_UNKNOWN && ev->get_keycode() != 0; } // Mouse @@ -455,7 +470,7 @@ void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_s if (p_cursor.is_valid()) { - Map<CursorShape, Vector<Variant> >::Element *cursor_c = cursors_cache.find(p_shape); + Map<CursorShape, Vector<Variant>>::Element *cursor_c = cursors_cache.find(p_shape); if (cursor_c) { if (cursor_c->get()[0] == p_cursor && cursor_c->get()[1] == p_hotspot) { @@ -466,7 +481,7 @@ void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_s cursors_cache.erase(p_shape); } - Ref<Texture> texture = p_cursor; + Ref<Texture2D> texture = p_cursor; Ref<AtlasTexture> atlas_texture = p_cursor; Ref<Image> image; Size2 texture_size; @@ -523,17 +538,13 @@ void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_s png_meta.height = texture_size.height; png_meta.format = PNG_FORMAT_RGBA; - PoolByteArray png; + PackedByteArray png; size_t len; - PoolByteArray::Read r = image->get_data().read(); - ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, r.ptr(), 0, NULL)); + PackedByteArray data = image->get_data(); + ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); png.resize(len); - PoolByteArray::Write w = png.write(); - ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, w.ptr(), &len, 0, r.ptr(), 0, NULL)); - w = PoolByteArray::Write(); - - r = png.read(); + ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); char *object_url; /* clang-format off */ @@ -548,9 +559,8 @@ void OS_JavaScript::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_s var string_on_wasm_heap = _malloc(length_bytes); setValue(PTR, string_on_wasm_heap, '*'); stringToUTF8(url, string_on_wasm_heap, length_bytes); - }, r.ptr(), len, &object_url); + }, png.ptr(), len, &object_url); /* clang-format on */ - r = PoolByteArray::Read(); String url = String::utf8(object_url) + "?" + itos(p_hotspot.x) + " " + itos(p_hotspot.y); @@ -811,12 +821,10 @@ int OS_JavaScript::get_video_driver_count() const { const char *OS_JavaScript::get_video_driver_name(int p_driver) const { switch (p_driver) { - case VIDEO_DRIVER_GLES3: - return "GLES3"; case VIDEO_DRIVER_GLES2: return "GLES2"; } - ERR_FAIL_V_MSG(NULL, "Invalid video driver index: " + itos(p_driver) + "."); + ERR_FAIL_V_MSG(nullptr, "Invalid video driver index: " + itos(p_driver) + "."); } // Audio @@ -879,52 +887,30 @@ int OS_JavaScript::get_current_video_driver() const { void OS_JavaScript::initialize_core() { OS_Unix::initialize_core(); - FileAccess::make_default<FileAccessBufferedFA<FileAccessUnix> >(FileAccess::ACCESS_RESOURCES); + FileAccess::make_default<FileAccessBufferedFA<FileAccessUnix>>(FileAccess::ACCESS_RESOURCES); } Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { +#if 0 EmscriptenWebGLContextAttributes attributes; emscripten_webgl_init_context_attributes(&attributes); - attributes.alpha = false; + attributes.alpha = GLOBAL_GET("display/window/per_pixel_transparency/allowed"); attributes.antialias = false; ERR_FAIL_INDEX_V(p_video_driver, VIDEO_DRIVER_MAX, ERR_INVALID_PARAMETER); - bool gles3 = true; - if (p_video_driver == VIDEO_DRIVER_GLES2) { - gles3 = false; + if (p_desired.layered) { + set_window_per_pixel_transparency_enabled(true); } bool gl_initialization_error = false; - while (true) { - if (gles3) { - if (RasterizerGLES3::is_viable() == OK) { - attributes.majorVersion = 2; - RasterizerGLES3::register_config(); - RasterizerGLES3::make_current(); - break; - } else { - if (GLOBAL_GET("rendering/quality/driver/fallback_to_gles2")) { - p_video_driver = VIDEO_DRIVER_GLES2; - gles3 = false; - continue; - } else { - gl_initialization_error = true; - break; - } - } - } else { - if (RasterizerGLES2::is_viable() == OK) { - attributes.majorVersion = 1; - RasterizerGLES2::register_config(); - RasterizerGLES2::make_current(); - break; - } else { - gl_initialization_error = true; - break; - } - } + if (RasterizerGLES2::is_viable() == OK) { + attributes.majorVersion = 1; + RasterizerGLES2::register_config(); + RasterizerGLES2::make_current(); + } else { + gl_initialization_error = true; } EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(GODOT_CANVAS_SELECTOR, &attributes); @@ -933,9 +919,8 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, } if (gl_initialization_error) { - OS::get_singleton()->alert("Your browser does not support any of the supported WebGL versions.\n" - "Please update your browser version.", - "Unable to initialize Video driver"); + OS::get_singleton()->alert("Your browser does not seem to support WebGL. Please update your browser version.", + "Unable to initialize video driver"); return ERR_UNAVAILABLE; } @@ -950,6 +935,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 @@ -964,6 +950,8 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, } else { set_window_size(get_window_size()); } +#endif + RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? char locale_ptr[16]; /* clang-format off */ @@ -974,18 +962,18 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, setenv("LANG", locale_ptr, true); AudioDriverManager::initialize(p_audio_driver); - VisualServer *visual_server = memnew(VisualServerRaster()); + RenderingServer *rendering_server = memnew(RenderingServerRaster()); input = memnew(InputDefault); EMSCRIPTEN_RESULT result; #define EM_CHECK(ev) \ if (result != EMSCRIPTEN_RESULT_SUCCESS) \ ERR_PRINT("Error while setting " #ev " callback: Code " + itos(result)); -#define SET_EM_CALLBACK(target, ev, cb) \ - result = emscripten_set_##ev##_callback(target, NULL, true, &cb); \ +#define SET_EM_CALLBACK(target, ev, cb) \ + result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \ EM_CHECK(ev) -#define SET_EM_CALLBACK_NOTARGET(ev, cb) \ - result = emscripten_set_##ev##_callback(NULL, true, &cb); \ +#define SET_EM_CALLBACK_NOTARGET(ev, cb) \ + result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ EM_CHECK(ev) // These callbacks from Emscripten's html5.h suffice to access most // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM @@ -994,10 +982,10 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, SET_EM_CALLBACK(GODOT_CANVAS_SELECTOR, mousedown, mouse_button_callback) SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, wheel, wheel_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, touchstart, touch_press_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, touchmove, touchmove_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, touchend, touch_press_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, touchcancel, touch_press_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) @@ -1021,14 +1009,14 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, update_clipboard(evt.clipboardData.getData('text')); }, true); }, - MainLoop::NOTIFICATION_WM_MOUSE_ENTER, - MainLoop::NOTIFICATION_WM_MOUSE_EXIT, - MainLoop::NOTIFICATION_WM_FOCUS_IN, - MainLoop::NOTIFICATION_WM_FOCUS_OUT + NOTIFICATION_WM_MOUSE_ENTER, + NOTIFICATION_WM_MOUSE_EXIT, + NOTIFICATION_WM_FOCUS_IN, + NOTIFICATION_WM_FOCUS_OUT ); /* clang-format on */ - visual_server->init(); + rendering_server->init(); return OK; } @@ -1084,7 +1072,7 @@ bool OS_JavaScript::main_loop_iterate() { strategy.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH; strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF; strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; - strategy.canvasResizedCallback = NULL; + strategy.canvasResizedCallback = nullptr; emscripten_enter_soft_fullscreen(GODOT_CANVAS_SELECTOR, &strategy); } else { emscripten_set_canvas_element_size(GODOT_CANVAS_SELECTOR, windowed_size.width, windowed_size.height); @@ -1133,8 +1121,8 @@ int OS_JavaScript::get_process_id() const { 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; + if (p_notification == NOTIFICATION_WM_MOUSE_ENTER || p_notification == NOTIFICATION_WM_MOUSE_EXIT) { + cursor_inside_canvas = p_notification == NOTIFICATION_WM_MOUSE_ENTER; } OS_JavaScript::get_singleton()->get_main_loop()->notification(p_notification); } @@ -1191,17 +1179,14 @@ void OS_JavaScript::set_icon(const Ref<Image> &p_icon) { png_meta.height = icon->get_height(); png_meta.format = PNG_FORMAT_RGBA; - PoolByteArray png; + PackedByteArray png; size_t len; - PoolByteArray::Read r = icon->get_data().read(); - ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, r.ptr(), 0, NULL)); + PackedByteArray data = icon->get_data(); + ERR_FAIL_COND(!png_image_write_get_memory_size(png_meta, len, 0, data.ptr(), 0, nullptr)); png.resize(len); - PoolByteArray::Write w = png.write(); - ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, w.ptr(), &len, 0, r.ptr(), 0, NULL)); - w = PoolByteArray::Write(); + ERR_FAIL_COND(!png_image_write_to_memory(&png_meta, png.ptrw(), &len, 0, data.ptr(), 0, nullptr)); - r = png.read(); /* clang-format off */ EM_ASM_ARGS({ var PNG_PTR = $0; @@ -1217,7 +1202,7 @@ void OS_JavaScript::set_icon(const Ref<Image> &p_icon) { document.head.appendChild(link); } link.href = url; - }, r.ptr(), len); + }, png.ptr(), len); /* clang-format on */ } @@ -1257,24 +1242,6 @@ String OS_JavaScript::get_resource_dir() const { return "/"; } -OS::PowerState OS_JavaScript::get_power_state() { - - WARN_PRINT("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"); - return -1; -} - -int OS_JavaScript::get_power_percent_left() { - - WARN_PRINT("Power management is not supported for the HTML5 platform, defaulting to -1"); - return -1; -} - void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) { OS_JavaScript *os = get_singleton(); @@ -1315,8 +1282,9 @@ OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) { window_maximized = false; entering_fullscreen = false; just_exited_fullscreen = false; + transparency_enabled = false; - main_loop = NULL; + main_loop = nullptr; idb_available = false; sync_wait_time = -1; diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 5c02a292ee..81fe4cf0cc 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -32,10 +32,10 @@ #define OS_JAVASCRIPT_H #include "audio_driver_javascript.h" +#include "core/input/input_filter.h" #include "drivers/unix/os_unix.h" -#include "main/input_default.h" #include "servers/audio_server.h" -#include "servers/visual/rasterizer.h" +#include "servers/rendering/rasterizer.h" #include <emscripten/html5.h> @@ -46,12 +46,13 @@ class OS_JavaScript : public OS_Unix { bool window_maximized; bool entering_fullscreen; bool just_exited_fullscreen; + bool transparency_enabled; InputDefault *input; Ref<InputEventKey> deferred_key_event; CursorShape cursor_shape; String cursors[CURSOR_MAX]; - Map<CursorShape, Vector<Variant> > cursors_cache; + Map<CursorShape, Vector<Variant>> cursors_cache; Point2 touches[32]; Point2i last_click_pos; @@ -123,6 +124,9 @@ public: virtual void set_mouse_mode(MouseMode p_mode); virtual MouseMode get_mouse_mode() const; + virtual bool get_window_per_pixel_transparency_enabled() const; + virtual void set_window_per_pixel_transparency_enabled(bool p_enabled); + virtual bool has_touchscreen_ui_hint() const; virtual bool is_joy_known(int p_device); @@ -141,7 +145,7 @@ public: 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); + virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr); virtual Error kill(const ProcessID &p_pid); virtual int get_process_id() const; @@ -156,10 +160,6 @@ public: virtual String get_resource_dir() const; virtual String get_user_data_dir() const; - virtual OS::PowerState get_power_state(); - virtual int get_power_seconds_left(); - virtual int get_power_percent_left(); - void set_idb_available(bool p_idb_available); virtual bool is_userfs_persistent() const; diff --git a/platform/javascript/pre.js b/platform/javascript/pre.js deleted file mode 100644 index a870e676ea..0000000000 --- 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. |