Browse Source

Merge pull request #40018 from Faless/js/more_improvements_4.0

HTML5 fixes, refactor, audio fallback, fixed FPS.
Rémi Verschelde 5 years ago
parent
commit
3fb5faaceb

+ 13 - 7
platform/javascript/audio_driver_javascript.cpp

@@ -36,6 +36,15 @@
 
 AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr;
 
+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 */
 }
@@ -293,9 +303,5 @@ Error AudioDriverJavaScript::capture_stop() {
 }
 
 AudioDriverJavaScript::AudioDriverJavaScript() {
-	_driver_id = 0;
-	internal_buffer = nullptr;
-	buffer_length = 0;
-
 	singleton = this;
 }

+ 4 - 3
platform/javascript/audio_driver_javascript.h

@@ -34,12 +34,13 @@
 #include "servers/audio_server.h"
 
 class AudioDriverJavaScript : public AudioDriver {
-	float *internal_buffer;
+	float *internal_buffer = nullptr;
 
-	int _driver_id;
-	int buffer_length;
+	int _driver_id = 0;
+	int buffer_length = 0;
 
 public:
+	static bool is_available();
 	void mix_to_js();
 	void process_capture(float sample);
 

+ 47 - 54
platform/javascript/display_server_javascript.cpp

@@ -44,18 +44,15 @@
 #define DOM_BUTTON_XBUTTON1 3
 #define DOM_BUTTON_XBUTTON2 4
 
+char DisplayServerJavaScript::canvas_id[256] = { 0 };
+static bool cursor_inside_canvas = true;
+
 DisplayServerJavaScript *DisplayServerJavaScript::get_singleton() {
 	return static_cast<DisplayServerJavaScript *>(DisplayServer::get_singleton());
 }
 
 // Window (canvas)
-extern "C" EMSCRIPTEN_KEEPALIVE void _set_canvas_id(uint8_t *p_data, int p_data_size) {
-	DisplayServerJavaScript *display = DisplayServerJavaScript::get_singleton();
-	display->canvas_id.parse_utf8((const char *)p_data, p_data_size);
-	display->canvas_id = "#" + display->canvas_id;
-}
-
-static void focus_canvas() {
+void DisplayServerJavaScript::focus_canvas() {
 	/* clang-format off */
 	EM_ASM(
 		Module['canvas'].focus();
@@ -63,7 +60,7 @@ static void focus_canvas() {
 	/* clang-format on */
 }
 
-static bool is_canvas_focused() {
+bool DisplayServerJavaScript::is_canvas_focused() {
 	/* clang-format off */
 	return EM_ASM_INT_V(
 		return document.activeElement == Module['canvas'];
@@ -71,8 +68,21 @@ static bool is_canvas_focused() {
 	/* clang-format on */
 }
 
-static Point2 compute_position_in_canvas(int x, int y) {
-	DisplayServerJavaScript *display = DisplayServerJavaScript::get_singleton();
+bool DisplayServerJavaScript::check_size_force_redraw() {
+	int canvas_width;
+	int canvas_height;
+	emscripten_get_canvas_element_size(DisplayServerJavaScript::canvas_id, &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(DisplayServerJavaScript::canvas_id, canvas_width, canvas_height);
+		return true;
+	}
+	return false;
+}
+
+Point2 DisplayServerJavaScript::compute_position_in_canvas(int p_x, int p_y) {
 	int canvas_x = EM_ASM_INT({
 		return Module['canvas'].getBoundingClientRect().x;
 	});
@@ -81,23 +91,22 @@ static Point2 compute_position_in_canvas(int x, int y) {
 	});
 	int canvas_width;
 	int canvas_height;
-	emscripten_get_canvas_element_size(display->canvas_id.utf8().get_data(), &canvas_width, &canvas_height);
+	emscripten_get_canvas_element_size(canvas_id, &canvas_width, &canvas_height);
 
 	double element_width;
 	double element_height;
-	emscripten_get_element_css_size(display->canvas_id.utf8().get_data(), &element_width, &element_height);
+	emscripten_get_element_css_size(canvas_id, &element_width, &element_height);
 
-	return Point2((int)(canvas_width / element_width * (x - canvas_x)),
-			(int)(canvas_height / element_height * (y - canvas_y)));
+	return Point2((int)(canvas_width / element_width * (p_x - canvas_x)),
+			(int)(canvas_height / element_height * (p_y - canvas_y)));
 }
 
-static bool cursor_inside_canvas = true;
-
 EM_BOOL DisplayServerJavaScript::fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data) {
 	DisplayServerJavaScript *display = get_singleton();
 	// Empty ID is canvas.
 	String target_id = String::utf8(p_event->id);
-	if (target_id.empty() || "#" + target_id == display->canvas_id) {
+	String canvas_str_id = String::utf8(canvas_id);
+	if (target_id.empty() || target_id == canvas_str_id) {
 		// This event property is the only reliable data on
 		// browser fullscreen state.
 		if (p_event->isFullscreen) {
@@ -131,14 +140,14 @@ extern "C" EMSCRIPTEN_KEEPALIVE void _drop_files_callback(char *p_filev[], int p
 // Keys
 
 template <typename T>
-static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event) {
+void DisplayServerJavaScript::dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event) {
 	godot_event->set_shift(emscripten_event_ptr->shiftKey);
 	godot_event->set_alt(emscripten_event_ptr->altKey);
 	godot_event->set_control(emscripten_event_ptr->ctrlKey);
 	godot_event->set_metakey(emscripten_event_ptr->metaKey);
 }
 
-static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) {
+Ref<InputEventKey> DisplayServerJavaScript::setup_key_event(const EmscriptenKeyboardEvent *emscripten_event) {
 	Ref<InputEventKey> ev;
 	ev.instance();
 	ev->set_echo(emscripten_event->repeat);
@@ -289,7 +298,7 @@ EM_BOOL DisplayServerJavaScript::mousemove_callback(int p_event_type, const Emsc
 }
 
 // Cursor
-static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape) {
+const char *DisplayServerJavaScript::godot2dom_cursor(DisplayServer::CursorShape p_shape) {
 	switch (p_shape) {
 		case DisplayServer::CURSOR_ARROW:
 			return "auto";
@@ -330,7 +339,7 @@ static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape) {
 	}
 }
 
-static void set_css_cursor(const char *p_cursor) {
+void DisplayServerJavaScript::set_css_cursor(const char *p_cursor) {
 	/* clang-format off */
 	EM_ASM_({
 		Module['canvas'].style.cursor = UTF8ToString($0);
@@ -338,7 +347,7 @@ static void set_css_cursor(const char *p_cursor) {
 	/* clang-format on */
 }
 
-static bool is_css_cursor_hidden() {
+bool DisplayServerJavaScript::is_css_cursor_hidden() const {
 	/* clang-format off */
 	return EM_ASM_INT({
 		return Module['canvas'].style.cursor === 'none';
@@ -820,23 +829,6 @@ DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_dr
 }
 
 DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) {
-	/* clang-format off */
-	EM_ASM({
-		const canvas = Module['canvas'];
-		var enc = new TextEncoder("utf-8");
-		var buffer = new Uint8Array(enc.encode(canvas.id));
-		var len = buffer.byteLength;
-		var out = _malloc(len);
-		HEAPU8.set(buffer, out);
-		ccall("_set_canvas_id",
-			"void",
-			["number", "number"],
-			[out, len]
-		);
-		_free(out);
-	});
-	/* clang-format on */
-
 	RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu?
 #if 0
 	EmscriptenWebGLContextAttributes attributes;
@@ -859,7 +851,7 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive
 		gl_initialization_error = true;
 	}
 
-	EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(canvas_id.utf8().get_data(), &attributes);
+	EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(canvas_id, &attributes);
 	if (emscripten_webgl_make_context_current(ctx) != EMSCRIPTEN_RESULT_SUCCESS) {
 		gl_initialization_error = true;
 	}
@@ -881,7 +873,6 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive
 	}
 
 	EMSCRIPTEN_RESULT result;
-	CharString id = canvas_id.utf8();
 #define EM_CHECK(ev)                         \
 	if (result != EMSCRIPTEN_RESULT_SUCCESS) \
 		ERR_PRINT("Error while setting " #ev " callback: Code " + itos(result));
@@ -895,16 +886,16 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive
 	// JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM
 	// is used below.
 	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback)
-	SET_EM_CALLBACK(id.get_data(), mousedown, mouse_button_callback)
+	SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback)
 	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback)
-	SET_EM_CALLBACK(id.get_data(), wheel, wheel_callback)
-	SET_EM_CALLBACK(id.get_data(), touchstart, touch_press_callback)
-	SET_EM_CALLBACK(id.get_data(), touchmove, touchmove_callback)
-	SET_EM_CALLBACK(id.get_data(), touchend, touch_press_callback)
-	SET_EM_CALLBACK(id.get_data(), touchcancel, touch_press_callback)
-	SET_EM_CALLBACK(id.get_data(), keydown, keydown_callback)
-	SET_EM_CALLBACK(id.get_data(), keypress, keypress_callback)
-	SET_EM_CALLBACK(id.get_data(), keyup, keyup_callback)
+	SET_EM_CALLBACK(canvas_id, wheel, wheel_callback)
+	SET_EM_CALLBACK(canvas_id, touchstart, touch_press_callback)
+	SET_EM_CALLBACK(canvas_id, touchmove, touchmove_callback)
+	SET_EM_CALLBACK(canvas_id, touchend, touch_press_callback)
+	SET_EM_CALLBACK(canvas_id, touchcancel, touch_press_callback)
+	SET_EM_CALLBACK(canvas_id, keydown, keydown_callback)
+	SET_EM_CALLBACK(canvas_id, keypress, keypress_callback)
+	SET_EM_CALLBACK(canvas_id, keyup, keyup_callback)
 	SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, fullscreenchange, fullscreen_change_callback)
 	SET_EM_CALLBACK_NOTARGET(gamepadconnected, gamepad_change_callback)
 	SET_EM_CALLBACK_NOTARGET(gamepaddisconnected, gamepad_change_callback)
@@ -1012,7 +1003,7 @@ Size2i DisplayServerJavaScript::screen_get_size(int p_screen) const {
 
 Rect2i DisplayServerJavaScript::screen_get_usable_rect(int p_screen) const {
 	int canvas[2];
-	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);
+	emscripten_get_canvas_element_size(canvas_id, canvas, canvas + 1);
 	return Rect2i(0, 0, canvas[0], canvas[1]);
 }
 
@@ -1103,12 +1094,14 @@ Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const {
 }
 
 void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) {
-	emscripten_set_canvas_element_size(canvas_id.utf8().get_data(), p_size.x, p_size.y);
+	last_width = p_size.x;
+	last_height = p_size.y;
+	emscripten_set_canvas_element_size(canvas_id, p_size.x, p_size.y);
 }
 
 Size2i DisplayServerJavaScript::window_get_size(WindowID p_window) const {
 	int canvas[2];
-	emscripten_get_canvas_element_size(canvas_id.utf8().get_data(), canvas, canvas + 1);
+	emscripten_get_canvas_element_size(canvas_id, canvas, canvas + 1);
 	return Size2(canvas[0], canvas[1]);
 }
 
@@ -1134,7 +1127,7 @@ void DisplayServerJavaScript::window_set_mode(WindowMode p_mode, WindowID p_wind
 			strategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
 			strategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT;
 			strategy.canvasResizedCallback = nullptr;
-			EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(canvas_id.utf8().get_data(), false, &strategy);
+			EMSCRIPTEN_RESULT result = emscripten_request_fullscreen_strategy(canvas_id, 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.");
 		} break;

+ 19 - 1
platform/javascript/display_server_javascript.h

@@ -53,6 +53,21 @@ class DisplayServerJavaScript : public DisplayServer {
 	double last_click_ms = 0;
 	int last_click_button_index = -1;
 
+	int last_width = 0;
+	int last_height = 0;
+
+	// utilities
+	static Point2 compute_position_in_canvas(int p_x, int p_y);
+	static void focus_canvas();
+	static bool is_canvas_focused();
+	template <typename T>
+	static void dom2godot_mod(T *emscripten_event_ptr, Ref<InputEventWithModifiers> godot_event);
+	static Ref<InputEventKey> setup_key_event(const EmscriptenKeyboardEvent *emscripten_event);
+	static const char *godot2dom_cursor(DisplayServer::CursorShape p_shape);
+	static void set_css_cursor(const char *p_cursor);
+	bool is_css_cursor_hidden() const;
+
+	// events
 	static EM_BOOL fullscreen_change_callback(int p_event_type, const EmscriptenFullscreenChangeEvent *p_event, void *p_user_data);
 
 	static EM_BOOL keydown_callback(int p_event_type, const EmscriptenKeyboardEvent *p_event, void *p_user_data);
@@ -81,17 +96,20 @@ protected:
 public:
 	// Override return type to make writing static callbacks less tedious.
 	static DisplayServerJavaScript *get_singleton();
+	static char canvas_id[256];
 
 	WindowMode window_mode = WINDOW_MODE_WINDOWED;
 
 	String clipboard;
-	String canvas_id;
 
 	Callable window_event_callback;
 	Callable input_event_callback;
 	Callable input_text_callback;
 	Callable drop_files_callback;
 
+	// utilities
+	bool check_size_force_redraw();
+
 	// from DisplayServer
 	virtual void alert(const String &p_alert, const String &p_title = "ALERT!");
 	virtual bool has_feature(Feature p_feature) const;

+ 47 - 5
platform/javascript/javascript_main.cpp

@@ -30,11 +30,13 @@
 
 #include "core/io/resource_loader.h"
 #include "main/main.h"
-#include "os_javascript.h"
+#include "platform/javascript/display_server_javascript.h"
+#include "platform/javascript/os_javascript.h"
 
 #include <emscripten/emscripten.h>
 
 static OS_JavaScript *os = nullptr;
+static uint64_t target_ticks = 0;
 
 void exit_callback() {
 	emscripten_cancel_main_loop(); // After this, we can exit!
@@ -46,12 +48,32 @@ void exit_callback() {
 }
 
 void main_loop_callback() {
+	uint64_t current_ticks = os->get_ticks_usec();
+
+	bool force_draw = DisplayServerJavaScript::get_singleton()->check_size_force_redraw();
+	if (force_draw) {
+		Main::force_redraw();
+	} else if (current_ticks < target_ticks) {
+		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.
+		/* 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({
@@ -79,13 +101,35 @@ 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();
-	emscripten_resume_main_loop();
 	// 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();
 }
 
 int main(int argc, char *argv[]) {
+	// Create and mount userfs immediately.
+	EM_ASM({
+		FS.mkdir('/userfs');
+		FS.mount(IDBFS, {}, '/userfs');
+	});
+
+	// Configure locale.
+	char locale_ptr[16];
+	/* clang-format off */
+	EM_ASM({
+		stringToUTF8(Module['locale'], $0, 16);
+	}, locale_ptr);
+	/* clang-format on */
+	setenv("LANG", locale_ptr, true);
+
+	// Ensure the canvas ID.
+	/* clang-format off */
+	EM_ASM({
+		stringToUTF8("#" + Module['canvas'].id, $0, 255);
+	}, DisplayServerJavaScript::canvas_id);
+	/* clang-format on */
+
 	os = new OS_JavaScript();
 	Main::setup(argv[0], argc - 1, &argv[1], false);
 	emscripten_set_main_loop(main_loop_callback, -1, false);
@@ -95,8 +139,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 : ""]);

+ 14 - 11
platform/javascript/os_javascript.cpp

@@ -74,18 +74,12 @@ void OS_JavaScript::initialize() {
 	EngineDebugger::register_uri_handler("ws://", RemoteDebuggerPeerWebSocket::create);
 	EngineDebugger::register_uri_handler("wss://", RemoteDebuggerPeerWebSocket::create);
 #endif
-
-	char locale_ptr[16];
-	/* clang-format off */
-	EM_ASM({
-		stringToUTF8(Module['locale'], $0, 16);
-	}, locale_ptr);
-	/* clang-format on */
-	setenv("LANG", locale_ptr, true);
 }
 
 void OS_JavaScript::resume_audio() {
-	audio_driver_javascript.resume();
+	if (audio_driver_javascript) {
+		audio_driver_javascript->resume();
+	}
 }
 
 void OS_JavaScript::set_main_loop(MainLoop *p_main_loop) {
@@ -133,11 +127,17 @@ void OS_JavaScript::delete_main_loop() {
 
 void OS_JavaScript::finalize_async() {
 	finalizing = true;
-	audio_driver_javascript.finish_async();
+	if (audio_driver_javascript) {
+		audio_driver_javascript->finish_async();
+	}
 }
 
 void OS_JavaScript::finalize() {
 	delete_main_loop();
+	if (audio_driver_javascript) {
+		memdelete(audio_driver_javascript);
+		audio_driver_javascript = nullptr;
+	}
 }
 
 // Miscellaneous
@@ -246,7 +246,10 @@ void OS_JavaScript::initialize_joypads() {
 }
 
 OS_JavaScript::OS_JavaScript() {
-	AudioDriverManager::add_driver(&audio_driver_javascript);
+	if (AudioDriverJavaScript::is_available()) {
+		audio_driver_javascript = memnew(AudioDriverJavaScript);
+		AudioDriverManager::add_driver(audio_driver_javascript);
+	}
 
 	Vector<Logger *> loggers;
 	loggers.push_back(memnew(StdLogger));

+ 4 - 1
platform/javascript/os_javascript.h

@@ -40,7 +40,7 @@
 
 class OS_JavaScript : public OS_Unix {
 	MainLoop *main_loop = nullptr;
-	AudioDriverJavaScript audio_driver_javascript;
+	AudioDriverJavaScript *audio_driver_javascript = nullptr;
 
 	bool finalizing = false;
 	bool idb_available = false;
@@ -83,6 +83,9 @@ public:
 	String get_executable_path() const;
 	virtual Error shell_open(String p_uri);
 	virtual String get_name() const;
+	// Override default OS implementation which would block the main thread with delay_usec.
+	// Implemented in javascript_main.cpp loop callback instead.
+	virtual void add_frame_delay(bool p_can_draw) {}
 	virtual bool can_draw() const;
 
 	virtual String get_cache_path() const;

+ 1 - 0
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;