From 093c463ebf3da960b12274b7f7fe9a5ddafac0bb Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Wed, 1 Jul 2020 15:40:30 +0200 Subject: [PATCH 1/5] [HTML5] Early FS initialization. So that "/userfs" is created and mounted before `setup`. --- platform/javascript/javascript_main.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index bb889fcf7c0..052ed24279b 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -120,9 +120,12 @@ extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { } int main(int argc, char *argv[]) { - + // Create and mount userfs immediately. + EM_ASM({ + FS.mkdir('/userfs'); + FS.mount(IDBFS, {}, '/userfs'); + }); 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. @@ -131,8 +134,6 @@ int main(int argc, char *argv[]) { // run the 'main_after_fs_sync' function. /* clang-format off */ EM_ASM({ - FS.mkdir('/userfs'); - FS.mount(IDBFS, {}, '/userfs'); FS.syncfs(true, function(err) { requestAnimationFrame(function() { ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""]); From c7d2767ab968de04a16f4e05c474e9277e8c2666 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Mon, 29 Jun 2020 18:54:20 +0200 Subject: [PATCH 2/5] [JS] Check canvas size each loop, force redraw. Remove ResizeObserver, fix compatibility issues, achieve smoother resizing. --- platform/javascript/javascript_main.cpp | 5 +++- platform/javascript/os_javascript.cpp | 31 ++++++++++++------------- platform/javascript/os_javascript.h | 4 ++++ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 052ed24279b..f1867e2b41e 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -59,6 +59,10 @@ void exit_callback() { void main_loop_callback() { + bool force_draw = os->check_size_force_redraw(); + if (force_draw) { + Main::force_redraw(); + } if (os->main_loop_iterate()) { emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async. EM_ASM({ @@ -106,7 +110,6 @@ extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { EM_ASM({ stringToUTF8(Module['locale'], $0, 16); }, locale_ptr); - /* clang-format on */ setenv("LANG", locale_ptr, true); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index bc7cfb8f08a..3508639a4e8 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -94,18 +94,22 @@ static Point2 compute_position_in_canvas(int x, int y) { (int)(canvas_height / element_height * (y - canvas_y))); } -static bool cursor_inside_canvas = true; - -extern "C" EMSCRIPTEN_KEEPALIVE void _canvas_resize_callback() { - OS_JavaScript *os = OS_JavaScript::get_singleton(); +bool OS_JavaScript::check_size_force_redraw() { int canvas_width; int canvas_height; - // Update the framebuffer size. - emscripten_get_canvas_element_size(os->canvas_id.utf8().get_data(), &canvas_width, &canvas_height); - emscripten_set_canvas_element_size(os->canvas_id.utf8().get_data(), canvas_width, canvas_height); - Main::force_redraw(); + emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), &canvas_width, &canvas_height); + if (last_width != canvas_width || last_height != canvas_height) { + last_width = canvas_width; + last_height = canvas_height; + // Update the framebuffer size and for redraw. + emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), canvas_width, canvas_height); + return true; + } + return false; } +static bool cursor_inside_canvas = true; + EM_BOOL OS_JavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) { OS_JavaScript *os = get_singleton(); @@ -1062,12 +1066,6 @@ Error OS_JavaScript::initialize(const VideoMode &p_desired, int p_video_driver, Module.listeners['drop'] = Module.drop_handler; // Defined in native/utils.js canvas.addEventListener('dragover', Module.listeners['dragover'], false); canvas.addEventListener('drop', Module.listeners['drop'], false); - // Resize - const resize_callback = cwrap('_canvas_resize_callback', null, []); - Module.resize_observer = new window['ResizeObserver'](function(elements) { - resize_callback(); - }); - Module.resize_observer.observe(canvas); // Quit request Module['request_quit'] = function() { send_notification(notifications[notifications.length - 1]); @@ -1167,8 +1165,6 @@ void OS_JavaScript::finalize_async() { } }); Module.listeners = {}; - Module.resize_observer.unobserve(canvas); - delete Module.resize_observer; }); audio_driver_javascript.finish_async(); } @@ -1409,6 +1405,9 @@ OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) { last_click_ms = 0; last_click_pos = Point2(-100, -100); + last_width = 0; + last_height = 0; + window_maximized = false; entering_fullscreen = false; just_exited_fullscreen = false; diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 9fcda31c4d5..fe8720d4942 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -61,6 +61,9 @@ class OS_JavaScript : public OS_Unix { double last_click_ms; int last_click_button_index; + int last_width; + int last_height; + MainLoop *main_loop; int video_driver_index; AudioDriverJavaScript audio_driver_javascript; @@ -105,6 +108,7 @@ protected: public: String canvas_id; void finalize_async(); + bool check_size_force_redraw(); // Override return type to make writing static callbacks less tedious. static OS_JavaScript *get_singleton(); From 399e2c1db095455c7db2bad18e20832b19d7eb48 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Mon, 29 Jun 2020 21:00:20 +0200 Subject: [PATCH 3/5] Limit FPS in JS by skipping iterations. --- platform/javascript/javascript_main.cpp | 16 ++++++++++++---- platform/javascript/os_javascript.h | 1 + 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index f1867e2b41e..33a7343eccf 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -30,11 +30,12 @@ #include "core/io/resource_loader.h" #include "main/main.h" -#include "os_javascript.h" +#include "platform/javascript/os_javascript.h" #include static OS_JavaScript *os = NULL; +static uint64_t target_ticks = 0; // Files drop (implemented in JS for now). extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p_filec) { @@ -58,10 +59,18 @@ void exit_callback() { } void main_loop_callback() { + uint64_t current_ticks = os->get_ticks_usec(); bool force_draw = os->check_size_force_redraw(); if (force_draw) { Main::force_redraw(); + } else if (current_ticks < target_ticks && !force_draw) { + return; // Skip frame. + } + + int target_fps = Engine::get_singleton()->get_target_fps(); + if (target_fps > 0) { + target_ticks += (uint64_t)(1000000 / target_fps); } if (os->main_loop_iterate()) { emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async. @@ -85,9 +94,6 @@ extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() { } 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()) { @@ -118,6 +124,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { ResourceLoader::set_abort_on_missing_resources(false); Main::start(); os->get_main_loop()->init(); + // Immediately run the first iteration. + // We are inside an animation frame, we want to immediately draw on the newly setup canvas. main_loop_callback(); emscripten_resume_main_loop(); } diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index fe8720d4942..acfe14343e2 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -163,6 +163,7 @@ public: String get_executable_path() const; virtual Error shell_open(String p_uri); virtual String get_name() const; + virtual void add_frame_delay(bool p_can_draw) {} virtual bool can_draw() const; virtual String get_cache_path() const; From 357e99a31e00f4193f218dd6d10e808717a6420f Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Mon, 29 Jun 2020 18:51:53 +0200 Subject: [PATCH 4/5] Use dummy driver when JS AudioContext is unavailable. --- .../javascript/audio_driver_javascript.cpp | 16 +++++++++--- platform/javascript/audio_driver_javascript.h | 1 + platform/javascript/javascript_main.cpp | 9 ++++++- platform/javascript/os_javascript.cpp | 25 +++++++++++++++---- platform/javascript/os_javascript.h | 4 ++- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 620135e1e17..51939ee824e 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -36,6 +36,15 @@ AudioDriverJavaScript *AudioDriverJavaScript::singleton = NULL; +bool AudioDriverJavaScript::is_available() { + return EM_ASM_INT({ + if (!(window.AudioContext || window.webkitAudioContext)) { + return 0; + } + return 1; + }) != 0; +} + const char *AudioDriverJavaScript::get_name() const { return "JavaScript"; } @@ -207,12 +216,14 @@ void AudioDriverJavaScript::finish_async() { /* clang-format off */ EM_ASM({ - var ref = Module.IDHandler.get($0); + const id = $0; + var ref = Module.IDHandler.get(id); Module.async_finish.push(new Promise(function(accept, reject) { if (!ref) { - console.log("Ref not found!", $0, Module.IDHandler); + console.log("Ref not found!", id, Module.IDHandler); setTimeout(accept, 0); } else { + Module.IDHandler.remove(id); const context = ref['context']; // Disconnect script and input. ref['script'].disconnect(); @@ -226,7 +237,6 @@ void AudioDriverJavaScript::finish_async() { }); } })); - Module.IDHandler.remove($0); }, id); /* clang-format on */ } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index c23d46fd191..6c9a9dbbeb8 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -41,6 +41,7 @@ class AudioDriverJavaScript : public AudioDriver { int buffer_length; public: + static bool is_available(); void mix_to_js(); void process_capture(float sample); diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 33a7343eccf..87699271a02 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -74,10 +74,17 @@ void main_loop_callback() { } if (os->main_loop_iterate()) { emscripten_cancel_main_loop(); // Cancel current loop and wait for finalize_async. + /* clang-format off */ EM_ASM({ // This will contain the list of operations that need to complete before cleanup. - Module.async_finish = []; + Module.async_finish = [ + // Always contains at least one async promise, to avoid firing immediately if nothing is added. + new Promise(function(accept, reject) { + setTimeout(accept, 0); + }) + ]; }); + /* clang-format on */ os->get_main_loop()->finish(); os->finalize_async(); // Will add all the async finish functions. EM_ASM({ diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 3508639a4e8..d6b806a8f79 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -297,7 +297,7 @@ EM_BOOL OS_JavaScript::keydown_callback(int p_event_type, const EmscriptenKeyboa } os->input->parse_input_event(ev); // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); + os->resume_audio(); return true; } @@ -390,7 +390,7 @@ EM_BOOL OS_JavaScript::mouse_button_callback(int p_event_type, const EmscriptenM os->input->parse_input_event(ev); // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); + os->resume_audio(); // Prevent multi-click text selection and wheel-click scrolling anchor. // Context menu is prevented through contextmenu event. return true; @@ -742,7 +742,7 @@ EM_BOOL OS_JavaScript::touch_press_callback(int p_event_type, const EmscriptenTo os->input->parse_input_event(ev); } // Resume audio context after input in case autoplay was denied. - os->audio_driver_javascript.resume(); + os->resume_audio(); return true; } @@ -1099,6 +1099,12 @@ MainLoop *OS_JavaScript::get_main_loop() const { return main_loop; } +void OS_JavaScript::resume_audio() { + if (audio_driver_javascript) { + audio_driver_javascript->resume(); + } +} + bool OS_JavaScript::main_loop_iterate() { if (is_userfs_persistent() && sync_wait_time >= 0) { @@ -1166,7 +1172,9 @@ void OS_JavaScript::finalize_async() { }); Module.listeners = {}; }); - audio_driver_javascript.finish_async(); + if (audio_driver_javascript) { + audio_driver_javascript->finish_async(); + } } void OS_JavaScript::finalize() { @@ -1176,6 +1184,9 @@ void OS_JavaScript::finalize() { emscripten_webgl_commit_frame(); memdelete(visual_server); emscripten_webgl_destroy_context(webgl_ctx); + if (audio_driver_javascript) { + memdelete(audio_driver_javascript); + } } // Miscellaneous @@ -1415,11 +1426,15 @@ OS_JavaScript::OS_JavaScript(int p_argc, char *p_argv[]) { main_loop = NULL; visual_server = NULL; + audio_driver_javascript = NULL; idb_available = false; sync_wait_time = -1; - AudioDriverManager::add_driver(&audio_driver_javascript); + if (AudioDriverJavaScript::is_available()) { + audio_driver_javascript = memnew(AudioDriverJavaScript); + AudioDriverManager::add_driver(audio_driver_javascript); + } Vector loggers; loggers.push_back(memnew(StdLogger)); diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index acfe14343e2..d27d9958e3d 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -66,7 +66,7 @@ class OS_JavaScript : public OS_Unix { MainLoop *main_loop; int video_driver_index; - AudioDriverJavaScript audio_driver_javascript; + AudioDriverJavaScript *audio_driver_javascript; VisualServer *visual_server; bool idb_available; @@ -93,6 +93,8 @@ class OS_JavaScript : public OS_Unix { static void file_access_close_callback(const String &p_file, int p_flags); protected: + void resume_audio(); + virtual int get_current_video_driver() const; virtual void initialize_core(); From d06ad4075714fa81cc966162e5aa6f757e42a271 Mon Sep 17 00:00:00 2001 From: Fabio Alessandrelli Date: Wed, 1 Jul 2020 13:19:58 +0200 Subject: [PATCH 5/5] Add default 50ms output_latency web override. Hopefully a good tradeoff between latency and performance on most browsers. --- servers/audio_server.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/servers/audio_server.cpp b/servers/audio_server.cpp index c0aa0424787..c4f1a95ce4a 100644 --- a/servers/audio_server.cpp +++ b/servers/audio_server.cpp @@ -184,6 +184,7 @@ void AudioDriverManager::initialize(int p_driver) { GLOBAL_DEF_RST("audio/enable_audio_input", false); GLOBAL_DEF_RST("audio/mix_rate", DEFAULT_MIX_RATE); GLOBAL_DEF_RST("audio/output_latency", DEFAULT_OUTPUT_LATENCY); + GLOBAL_DEF_RST("audio/output_latency.web", 50); // Safer default output_latency for web. int failed_driver = -1;