Просмотр исходного кода

MacOS: Embedded window support.

Stuart Carnie 2 месяцев назад
Родитель
Сommit
00e1fdec2c
37 измененных файлов с 3646 добавлено и 360 удалено
  1. 17 0
      core/input/input_enums.h
  2. 26 0
      core/input/input_event.h
  3. 474 0
      core/input/input_event_codec.cpp
  4. 49 0
      core/input/input_event_codec.h
  5. 2 1
      drivers/apple/joypad_apple.h
  6. 17 13
      drivers/apple/joypad_apple.mm
  7. 2 1
      editor/debugger/script_editor_debugger.cpp
  8. 9 1
      editor/editor_node.cpp
  9. 81 66
      editor/plugins/embedded_process.cpp
  10. 53 26
      editor/plugins/embedded_process.h
  11. 67 31
      editor/plugins/game_view_plugin.cpp
  12. 14 5
      editor/plugins/game_view_plugin.h
  13. 9 0
      platform/macos/SCsub
  14. 2 2
      platform/macos/detect.py
  15. 231 0
      platform/macos/display_server_embedded.h
  16. 744 0
      platform/macos/display_server_embedded.mm
  17. 28 4
      platform/macos/display_server_macos.h
  18. 88 20
      platform/macos/display_server_macos.mm
  19. 75 0
      platform/macos/editor/embedded_game_view_plugin.h
  20. 154 0
      platform/macos/editor/embedded_game_view_plugin.mm
  21. 110 0
      platform/macos/editor/embedded_process_macos.h
  22. 248 0
      platform/macos/editor/embedded_process_macos.mm
  23. 70 0
      platform/macos/embedded_debugger.h
  24. 177 0
      platform/macos/embedded_debugger.mm
  25. 108 0
      platform/macos/embedded_gl_manager.h
  26. 271 0
      platform/macos/embedded_gl_manager.mm
  27. 7 0
      platform/macos/gl_manager_macos_angle.h
  28. 10 0
      platform/macos/godot_application.h
  29. 61 0
      platform/macos/godot_application.mm
  30. 5 11
      platform/macos/godot_application_delegate.h
  31. 47 73
      platform/macos/godot_application_delegate.mm
  32. 7 1
      platform/macos/godot_content_view.mm
  33. 63 9
      platform/macos/godot_main_macos.mm
  34. 3 2
      platform/macos/godot_window_delegate.mm
  35. 94 0
      platform/macos/macos_quartz_core_spi.h
  36. 41 21
      platform/macos/os_macos.h
  37. 182 73
      platform/macos/os_macos.mm

+ 17 - 0
core/input/input_enums.h

@@ -32,6 +32,23 @@
 
 #include "core/error/error_macros.h"
 
+enum class InputEventType {
+	INVALID = -1,
+	KEY,
+	MOUSE_BUTTON,
+	MOUSE_MOTION,
+	JOY_MOTION,
+	JOY_BUTTON,
+	SCREEN_TOUCH,
+	SCREEN_DRAG,
+	MAGNIFY_GESTURE,
+	PAN_GESTURE,
+	MIDI,
+	SHORTCUT,
+	ACTION,
+	MAX,
+};
+
 enum class HatDir {
 	UP = 0,
 	RIGHT = 1,

+ 26 - 0
core/input/input_event.h

@@ -89,6 +89,8 @@ public:
 
 	virtual bool accumulate(const Ref<InputEvent> &p_event) { return false; }
 
+	virtual InputEventType get_type() const { return InputEventType::INVALID; }
+
 	InputEvent() {}
 };
 
@@ -202,6 +204,8 @@ public:
 
 	static Ref<InputEventKey> create_reference(Key p_keycode_with_modifier_masks, bool p_physical = false);
 
+	InputEventType get_type() const final override { return InputEventType::KEY; }
+
 	InputEventKey() {}
 };
 
@@ -261,6 +265,8 @@ public:
 	virtual String as_text() const override;
 	virtual String to_string() override;
 
+	InputEventType get_type() const final override { return InputEventType::MOUSE_BUTTON; }
+
 	InputEventMouseButton() {}
 };
 
@@ -306,6 +312,8 @@ public:
 
 	virtual bool accumulate(const Ref<InputEvent> &p_event) override;
 
+	InputEventType get_type() const final override { return InputEventType::MOUSE_MOTION; }
+
 	InputEventMouseMotion() {}
 };
 
@@ -333,6 +341,8 @@ public:
 
 	static Ref<InputEventJoypadMotion> create_reference(JoyAxis p_axis, float p_value);
 
+	InputEventType get_type() const final override { return InputEventType::JOY_MOTION; }
+
 	InputEventJoypadMotion() {}
 };
 
@@ -363,6 +373,8 @@ public:
 
 	static Ref<InputEventJoypadButton> create_reference(JoyButton p_btn_index);
 
+	InputEventType get_type() const final override { return InputEventType::JOY_BUTTON; }
+
 	InputEventJoypadButton() {}
 };
 
@@ -392,6 +404,8 @@ public:
 	virtual String as_text() const override;
 	virtual String to_string() override;
 
+	InputEventType get_type() const final override { return InputEventType::SCREEN_TOUCH; }
+
 	InputEventScreenTouch() {}
 };
 
@@ -444,6 +458,8 @@ public:
 
 	virtual bool accumulate(const Ref<InputEvent> &p_event) override;
 
+	InputEventType get_type() const final override { return InputEventType::SCREEN_DRAG; }
+
 	InputEventScreenDrag() {}
 };
 
@@ -479,6 +495,8 @@ public:
 	virtual String as_text() const override;
 	virtual String to_string() override;
 
+	InputEventType get_type() const final override { return InputEventType::ACTION; }
+
 	InputEventAction() {}
 };
 
@@ -510,6 +528,8 @@ public:
 	virtual String as_text() const override;
 	virtual String to_string() override;
 
+	InputEventType get_type() const final override { return InputEventType::MAGNIFY_GESTURE; }
+
 	InputEventMagnifyGesture() {}
 };
 
@@ -528,6 +548,8 @@ public:
 	virtual String as_text() const override;
 	virtual String to_string() override;
 
+	InputEventType get_type() const final override { return InputEventType::PAN_GESTURE; }
+
 	InputEventPanGesture() {}
 };
 
@@ -574,6 +596,8 @@ public:
 	virtual String as_text() const override;
 	virtual String to_string() override;
 
+	InputEventType get_type() const final override { return InputEventType::MIDI; }
+
 	InputEventMIDI() {}
 };
 
@@ -592,5 +616,7 @@ public:
 	virtual String as_text() const override;
 	virtual String to_string() override;
 
+	InputEventType get_type() const final override { return InputEventType::SHORTCUT; }
+
 	InputEventShortcut();
 };

+ 474 - 0
core/input/input_event_codec.cpp

@@ -0,0 +1,474 @@
+/**************************************************************************/
+/*  input_event_codec.cpp                                                 */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "input_event_codec.h"
+
+#include "core/input/input.h"
+#include "core/io/marshalls.h"
+#include "core/os/os.h"
+
+enum class BoolShift : uint8_t {
+	SHIFT = 0,
+	CTRL,
+	ALT,
+	META,
+	ECHO,
+	PRESSED,
+	DOUBLE_CLICK,
+	PEN_INVERTED,
+};
+
+// cast operator for BoolShift to uint8_t
+inline uint8_t operator<<(uint8_t a, BoolShift b) {
+	return a << static_cast<uint8_t>(b);
+}
+
+uint8_t encode_key_modifier_state(Ref<InputEventWithModifiers> p_event) {
+	uint8_t bools = 0;
+	bools |= (uint8_t)p_event->is_shift_pressed() << BoolShift::SHIFT;
+	bools |= (uint8_t)p_event->is_ctrl_pressed() << BoolShift::CTRL;
+	bools |= (uint8_t)p_event->is_alt_pressed() << BoolShift::ALT;
+	bools |= (uint8_t)p_event->is_meta_pressed() << BoolShift::META;
+	return bools;
+}
+
+void decode_key_modifier_state(uint8_t bools, Ref<InputEventWithModifiers> p_event) {
+	p_event->set_shift_pressed(bools & (1 << BoolShift::SHIFT));
+	p_event->set_ctrl_pressed(bools & (1 << BoolShift::CTRL));
+	p_event->set_alt_pressed(bools & (1 << BoolShift::ALT));
+	p_event->set_meta_pressed(bools & (1 << BoolShift::META));
+}
+
+int encode_vector2(const Vector2 &p_vector, uint8_t *p_data) {
+	p_data += encode_float(p_vector.x, p_data);
+	encode_float(p_vector.y, p_data);
+	return sizeof(float) * 2;
+}
+
+const uint8_t *decode_vector2(Vector2 &r_vector, const uint8_t *p_data) {
+	r_vector.x = decode_float(p_data);
+	p_data += sizeof(float);
+	r_vector.y = decode_float(p_data);
+	p_data += sizeof(float);
+	return p_data;
+}
+
+void encode_input_event_key(const Ref<InputEventKey> &p_event, PackedByteArray &r_data) {
+	r_data.resize(19);
+
+	uint8_t *data = r_data.ptrw();
+	*data = (uint8_t)InputEventType::KEY;
+	data++;
+	uint8_t bools = encode_key_modifier_state(p_event);
+	bools |= (uint8_t)p_event->is_echo() << BoolShift::ECHO;
+	bools |= (uint8_t)p_event->is_pressed() << BoolShift::PRESSED;
+	*data = bools;
+	data++;
+	data += encode_uint32((uint32_t)p_event->get_keycode(), data);
+	data += encode_uint32((uint32_t)p_event->get_physical_keycode(), data);
+	data += encode_uint32((uint32_t)p_event->get_key_label(), data);
+	data += encode_uint32(p_event->get_unicode(), data);
+	*data = (uint8_t)p_event->get_location();
+	data++;
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - r_data.ptrw() >= r_data.size());
+}
+
+Error decode_input_event_key(const PackedByteArray &p_data, Ref<InputEventKey> &r_event) {
+	const uint8_t *data = p_data.ptr();
+	DEV_ASSERT(static_cast<InputEventType>(*data) == InputEventType::KEY);
+	data++; // Skip event type.
+
+	uint8_t bools = *data;
+	data++;
+	decode_key_modifier_state(bools, r_event);
+	r_event->set_echo(bools & (1 << BoolShift::ECHO));
+	r_event->set_pressed(bools & (1 << BoolShift::PRESSED));
+
+	Key keycode = (Key)decode_uint32(data);
+	data += sizeof(uint32_t);
+	Key physical_keycode = (Key)decode_uint32(data);
+	data += sizeof(uint32_t);
+	Key key_label = (Key)decode_uint32(data);
+	data += sizeof(uint32_t);
+	char32_t unicode = (char32_t)decode_uint32(data);
+	data += sizeof(uint32_t);
+	KeyLocation location = (KeyLocation)*data;
+
+	r_event->set_keycode(keycode);
+	r_event->set_physical_keycode(physical_keycode);
+	r_event->set_key_label(key_label);
+	r_event->set_unicode(unicode);
+	r_event->set_location(location);
+
+	return OK;
+}
+
+void encode_input_event_mouse_button(const Ref<InputEventMouseButton> &p_event, PackedByteArray &r_data) {
+	r_data.resize(12);
+
+	uint8_t *data = r_data.ptrw();
+	*data = (uint8_t)InputEventType::MOUSE_BUTTON;
+	data++;
+
+	uint8_t bools = encode_key_modifier_state(p_event);
+	bools |= (uint8_t)p_event->is_pressed() << BoolShift::PRESSED;
+	bools |= (uint8_t)p_event->is_double_click() << BoolShift::DOUBLE_CLICK;
+	*data = bools;
+	data++;
+
+	*data = (uint8_t)p_event->get_button_index();
+	data++;
+
+	// Rather than use encode_variant, we explicitly encode the Vector2,
+	// so decoding is easier. Specifically, we don't have to perform additional error
+	// checking for decoding the variant and then checking the variant type.
+	data += encode_vector2(p_event->get_position(), data);
+	*data = (uint8_t)p_event->get_button_mask();
+	data++;
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - r_data.ptrw() >= r_data.size());
+}
+
+Error decode_input_event_mouse_button(const PackedByteArray &p_data, Ref<InputEventMouseButton> &r_event) {
+	const uint8_t *data = p_data.ptr();
+	DEV_ASSERT(static_cast<InputEventType>(*data) == InputEventType::MOUSE_BUTTON);
+	data++; // Skip event type.
+
+	uint8_t bools = *data;
+	data++;
+	decode_key_modifier_state(bools, r_event);
+	r_event->set_pressed(bools & (1 << BoolShift::PRESSED));
+	r_event->set_double_click(bools & (1 << BoolShift::DOUBLE_CLICK));
+
+	r_event->set_button_index((MouseButton)*data);
+	data++;
+
+	Vector2 pos;
+	data = decode_vector2(pos, data);
+	r_event->set_position(pos);
+	r_event->set_global_position(pos); // these are the same
+	BitField<MouseButtonMask> button_mask = (MouseButtonMask)*data;
+	r_event->set_button_mask(button_mask);
+
+	return OK;
+}
+
+void encode_input_event_mouse_motion(const Ref<InputEventMouseMotion> &p_event, PackedByteArray &r_data) {
+	r_data.resize(31);
+
+	uint8_t *data = r_data.ptrw();
+	*data = (uint8_t)InputEventType::MOUSE_MOTION;
+	data++;
+
+	uint8_t bools = encode_key_modifier_state(p_event);
+	bools |= (uint8_t)p_event->get_pen_inverted() << BoolShift::PEN_INVERTED;
+	*data = bools;
+	data++;
+
+	// Rather than use encode_variant, we explicitly encode the Vector2,
+	// so decoding is easier. Specifically, we don't have to perform additional error
+	// checking for decoding the variant and then checking the variant type.
+	data += encode_vector2(p_event->get_position(), data);
+	data += encode_float(p_event->get_pressure(), data);
+	data += encode_vector2(p_event->get_tilt(), data);
+	data += encode_vector2(p_event->get_relative(), data);
+	*data = (uint8_t)p_event->get_button_mask();
+	data++;
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - r_data.ptrw() >= r_data.size());
+}
+
+void decode_input_event_mouse_motion(const PackedByteArray &p_data, Ref<InputEventMouseMotion> &r_event) {
+	Input *input = Input::get_singleton();
+
+	const uint8_t *data = p_data.ptr();
+	DEV_ASSERT(static_cast<InputEventType>(*data) == InputEventType::MOUSE_MOTION);
+	data++; // Skip event type.
+
+	uint8_t bools = *data;
+	data++;
+	decode_key_modifier_state(bools, r_event);
+	r_event->set_pen_inverted(bools & (1 << BoolShift::PEN_INVERTED));
+
+	{
+		Vector2 pos;
+		data = decode_vector2(pos, data);
+		r_event->set_position(pos);
+		r_event->set_global_position(pos); // these are the same
+	}
+	r_event->set_pressure(decode_float(data));
+	data += sizeof(float);
+	{
+		Vector2 tilt;
+		data = decode_vector2(tilt, data);
+		r_event->set_tilt(tilt);
+	}
+	r_event->set_velocity(input->get_last_mouse_velocity());
+	r_event->set_screen_velocity(input->get_last_mouse_screen_velocity());
+	{
+		Vector2 relative;
+		data = decode_vector2(relative, data);
+		r_event->set_relative(relative);
+		r_event->set_relative_screen_position(relative);
+	}
+	BitField<MouseButtonMask> button_mask = (MouseButtonMask)*data;
+	r_event->set_button_mask(button_mask);
+	data++;
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - p_data.ptr() >= p_data.size());
+}
+
+void encode_input_event_joypad_button(const Ref<InputEventJoypadButton> &p_event, PackedByteArray &r_data) {
+	r_data.resize(11);
+
+	uint8_t *data = r_data.ptrw();
+	*data = (uint8_t)InputEventType::JOY_BUTTON;
+	data++;
+
+	uint8_t bools = 0;
+	bools |= (uint8_t)p_event->is_pressed() << BoolShift::PRESSED;
+	*data = bools;
+	data++;
+
+	data += encode_uint64(p_event->get_device(), data);
+	*data = (uint8_t)p_event->get_button_index();
+	data++;
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - r_data.ptrw() >= r_data.size());
+}
+
+void decode_input_event_joypad_button(const PackedByteArray &p_data, Ref<InputEventJoypadButton> &r_event) {
+	const uint8_t *data = p_data.ptr();
+	DEV_ASSERT(static_cast<InputEventType>(*data) == InputEventType::JOY_BUTTON);
+	data++; // Skip event type.
+
+	uint8_t bools = *data;
+	data++;
+	r_event->set_pressed(bools & (1 << BoolShift::PRESSED));
+	r_event->set_device(decode_uint64(data));
+	data += sizeof(uint64_t);
+	r_event->set_button_index((JoyButton)*data);
+	data++;
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - p_data.ptr() >= p_data.size());
+}
+
+void encode_input_event_joypad_motion(const Ref<InputEventJoypadMotion> &p_event, PackedByteArray &r_data) {
+	r_data.resize(14);
+
+	uint8_t *data = r_data.ptrw();
+	*data = (uint8_t)InputEventType::JOY_MOTION;
+	data++;
+
+	data += encode_uint64(p_event->get_device(), data);
+	*data = (uint8_t)p_event->get_axis();
+	data++;
+	data += encode_float(p_event->get_axis_value(), data);
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - r_data.ptrw() >= r_data.size());
+}
+
+void decode_input_event_joypad_motion(const PackedByteArray &p_data, Ref<InputEventJoypadMotion> &r_event) {
+	const uint8_t *data = p_data.ptr();
+	DEV_ASSERT(static_cast<InputEventType>(*data) == InputEventType::JOY_MOTION);
+	data++; // Skip event type.
+
+	r_event->set_device(decode_uint64(data));
+	data += sizeof(uint64_t);
+	r_event->set_axis((JoyAxis)*data);
+	data++;
+	r_event->set_axis_value(decode_float(data));
+	data += sizeof(float);
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - p_data.ptr() >= p_data.size());
+}
+
+void encode_input_event_gesture_pan(const Ref<InputEventPanGesture> &p_event, PackedByteArray &r_data) {
+	r_data.resize(18);
+
+	uint8_t *data = r_data.ptrw();
+	*data = (uint8_t)InputEventType::PAN_GESTURE;
+	data++;
+
+	uint8_t bools = encode_key_modifier_state(p_event);
+	*data = bools;
+	data++;
+	data += encode_vector2(p_event->get_position(), data);
+	data += encode_vector2(p_event->get_delta(), data);
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - r_data.ptrw() >= r_data.size());
+}
+
+void decode_input_event_gesture_pan(const PackedByteArray &p_data, Ref<InputEventPanGesture> &r_event) {
+	const uint8_t *data = p_data.ptr();
+	DEV_ASSERT(static_cast<InputEventType>(*data) == InputEventType::PAN_GESTURE);
+	data++; // Skip event type.
+
+	uint8_t bools = *data;
+	data++;
+	decode_key_modifier_state(bools, r_event);
+
+	Vector2 pos;
+	data = decode_vector2(pos, data);
+	r_event->set_position(pos);
+	Vector2 delta;
+	data = decode_vector2(delta, data);
+	r_event->set_delta(delta);
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - p_data.ptr() >= p_data.size());
+}
+
+void encode_input_event_gesture_magnify(const Ref<InputEventMagnifyGesture> &p_event, PackedByteArray &r_data) {
+	r_data.resize(14);
+
+	uint8_t *data = r_data.ptrw();
+	*data = (uint8_t)InputEventType::MAGNIFY_GESTURE;
+	data++;
+
+	uint8_t bools = encode_key_modifier_state(p_event);
+	*data = bools;
+	data++;
+	data += encode_vector2(p_event->get_position(), data);
+	data += encode_float(p_event->get_factor(), data);
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - r_data.ptrw() >= r_data.size());
+}
+
+void decode_input_event_gesture_magnify(const PackedByteArray &p_data, Ref<InputEventMagnifyGesture> &r_event) {
+	const uint8_t *data = p_data.ptr();
+	DEV_ASSERT(static_cast<InputEventType>(*data) == InputEventType::MAGNIFY_GESTURE);
+	data++; // Skip event type.
+
+	uint8_t bools = *data;
+	data++;
+	decode_key_modifier_state(bools, r_event);
+
+	Vector2 pos;
+	data = decode_vector2(pos, data);
+	r_event->set_position(pos);
+	r_event->set_factor(decode_float(data));
+	data += sizeof(float);
+
+	// Assert we had enough space.
+	DEV_ASSERT(data - p_data.ptr() >= p_data.size());
+}
+
+bool encode_input_event(const Ref<InputEvent> &p_event, PackedByteArray &r_data) {
+	switch (p_event->get_type()) {
+		case InputEventType::KEY:
+			encode_input_event_key(p_event, r_data);
+			break;
+		case InputEventType::MOUSE_BUTTON:
+			encode_input_event_mouse_button(p_event, r_data);
+			break;
+		case InputEventType::MOUSE_MOTION:
+			encode_input_event_mouse_motion(p_event, r_data);
+			break;
+		case InputEventType::JOY_MOTION:
+			encode_input_event_joypad_motion(p_event, r_data);
+			break;
+		case InputEventType::JOY_BUTTON:
+			encode_input_event_joypad_button(p_event, r_data);
+			break;
+		case InputEventType::MAGNIFY_GESTURE:
+			encode_input_event_gesture_magnify(p_event, r_data);
+			break;
+		case InputEventType::PAN_GESTURE:
+			encode_input_event_gesture_pan(p_event, r_data);
+			break;
+		default:
+			return false;
+	}
+	return true;
+}
+
+void decode_input_event(const PackedByteArray &p_data, Ref<InputEvent> &r_event) {
+	const uint8_t *data = p_data.ptr();
+
+	switch (static_cast<InputEventType>(*data)) {
+		case InputEventType::KEY: {
+			Ref<InputEventKey> event;
+			event.instantiate();
+			decode_input_event_key(p_data, event);
+			r_event = event;
+		} break;
+		case InputEventType::MOUSE_BUTTON: {
+			Ref<InputEventMouseButton> event;
+			event.instantiate();
+			decode_input_event_mouse_button(p_data, event);
+			r_event = event;
+		} break;
+		case InputEventType::MOUSE_MOTION: {
+			Ref<InputEventMouseMotion> event;
+			event.instantiate();
+			decode_input_event_mouse_motion(p_data, event);
+			r_event = event;
+		} break;
+		case InputEventType::JOY_BUTTON: {
+			Ref<InputEventJoypadButton> event;
+			event.instantiate();
+			decode_input_event_joypad_button(p_data, event);
+			r_event = event;
+		} break;
+		case InputEventType::JOY_MOTION: {
+			Ref<InputEventJoypadMotion> event;
+			event.instantiate();
+			decode_input_event_joypad_motion(p_data, event);
+			r_event = event;
+		} break;
+		case InputEventType::PAN_GESTURE: {
+			Ref<InputEventPanGesture> event;
+			event.instantiate();
+			decode_input_event_gesture_pan(p_data, event);
+			r_event = event;
+		} break;
+		case InputEventType::MAGNIFY_GESTURE: {
+			Ref<InputEventMagnifyGesture> event;
+			event.instantiate();
+			decode_input_event_gesture_magnify(p_data, event);
+			r_event = event;
+		} break;
+		default: {
+			WARN_PRINT(vformat("Unknown event type %d.", static_cast<int>(*data)));
+		} break;
+	}
+}

+ 49 - 0
core/input/input_event_codec.h

@@ -0,0 +1,49 @@
+/**************************************************************************/
+/*  input_event_codec.h                                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "core/input/input_event.h"
+
+/**
+ * Encodes the input event as a byte array.
+ *
+ * Returns `true` if the event was successfully encoded, `false` otherwise.
+ */
+bool encode_input_event(const Ref<InputEvent> &p_event, PackedByteArray &r_data);
+void decode_input_event(const PackedByteArray &p_data, Ref<InputEvent> &r_event);
+
+void encode_input_event_key(const Ref<InputEventKey> &p_event, PackedByteArray &r_data);
+void encode_input_event_mouse_button(const Ref<InputEventMouseButton> &p_event, PackedByteArray &r_data);
+void encode_input_event_mouse_motion(const Ref<InputEventMouseMotion> &p_event, PackedByteArray &r_data);
+void encode_input_event_joypad_button(const Ref<InputEventJoypadButton> &p_event, PackedByteArray &r_data);
+void encode_input_event_joypad_motion(const Ref<InputEventJoypadMotion> &p_event, PackedByteArray &r_data);
+void encode_input_event_gesture_pan(const Ref<InputEventPanGesture> &p_event, PackedByteArray &r_data);
+void encode_input_event_gesture_magnify(const Ref<InputEventMagnifyGesture> &p_event, PackedByteArray &r_data);

+ 2 - 1
drivers/apple/joypad_apple.h

@@ -49,7 +49,8 @@ struct GameController {
 	bool double_nintendo_joycon_layout = false;
 	bool single_nintendo_joycon_layout = false;
 
-	bool axis_changed[(int)JoyAxis::MAX];
+	uint32_t axis_changed_mask = 0;
+	static_assert(static_cast<uint32_t>(JoyAxis::MAX) < 32, "JoyAxis::MAX must be less than 32");
 	double axis_value[(int)JoyAxis::MAX];
 
 	GameController(int p_joy_id, GCController *p_controller);

+ 17 - 13
drivers/apple/joypad_apple.mm

@@ -136,7 +136,6 @@ GameController::GameController(int p_joy_id, GCController *p_controller) :
 	force_feedback = NO;
 
 	for (int i = 0; i < (int)JoyAxis::MAX; i++) {
-		axis_changed[i] = false;
 		axis_value[i] = 0.0;
 	}
 	if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
@@ -169,36 +168,36 @@ GameController::GameController(int p_joy_id, GCController *p_controller) :
 
 	auto JOYSTICK_LEFT = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
 		if (axis_value[(int)JoyAxis::LEFT_X] != xValue) {
-			axis_changed[(int)JoyAxis::LEFT_X] = true;
+			axis_changed_mask |= (1 << (int)JoyAxis::LEFT_X);
 			axis_value[(int)JoyAxis::LEFT_X] = xValue;
 		}
 		if (axis_value[(int)JoyAxis::LEFT_Y] != -yValue) {
-			axis_changed[(int)JoyAxis::LEFT_Y] = true;
+			axis_changed_mask |= (1 << (int)JoyAxis::LEFT_Y);
 			axis_value[(int)JoyAxis::LEFT_Y] = -yValue;
 		}
 	};
 
 	auto JOYSTICK_RIGHT = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
 		if (axis_value[(int)JoyAxis::RIGHT_X] != xValue) {
-			axis_changed[(int)JoyAxis::RIGHT_X] = true;
+			axis_changed_mask |= (1 << (int)JoyAxis::RIGHT_X);
 			axis_value[(int)JoyAxis::RIGHT_X] = xValue;
 		}
 		if (axis_value[(int)JoyAxis::RIGHT_Y] != -yValue) {
-			axis_changed[(int)JoyAxis::RIGHT_Y] = true;
+			axis_changed_mask |= (1 << (int)JoyAxis::RIGHT_Y);
 			axis_value[(int)JoyAxis::RIGHT_Y] = -yValue;
 		}
 	};
 
 	auto TRIGGER_LEFT = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
 		if (axis_value[(int)JoyAxis::TRIGGER_LEFT] != value) {
-			axis_changed[(int)JoyAxis::TRIGGER_LEFT] = true;
+			axis_changed_mask |= (1 << (int)JoyAxis::TRIGGER_LEFT);
 			axis_value[(int)JoyAxis::TRIGGER_LEFT] = value;
 		}
 	};
 
 	auto TRIGGER_RIGHT = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
 		if (axis_value[(int)JoyAxis::TRIGGER_RIGHT] != value) {
-			axis_changed[(int)JoyAxis::TRIGGER_RIGHT] = true;
+			axis_changed_mask |= (1 << (int)JoyAxis::TRIGGER_RIGHT);
 			axis_value[(int)JoyAxis::TRIGGER_RIGHT] = value;
 		}
 	};
@@ -595,20 +594,25 @@ void JoypadApple::joypad_vibration_stop(GameController &p_joypad, uint64_t p_tim
 }
 
 void JoypadApple::process_joypads() {
+	Input *input = Input::get_singleton();
+
 	if (@available(macOS 11.0, iOS 14.0, tvOS 14.0, *)) {
 		for (KeyValue<int, GameController *> &E : joypads) {
 			int id = E.key;
 			GameController &joypad = *E.value;
 
-			for (int i = 0; i < (int)JoyAxis::MAX; i++) {
-				if (joypad.axis_changed[i]) {
-					joypad.axis_changed[i] = false;
-					Input::get_singleton()->joy_axis(id, (JoyAxis)i, joypad.axis_value[i]);
-				}
+			uint32_t changed = joypad.axis_changed_mask;
+			joypad.axis_changed_mask = 0;
+			// Loop over changed axes.
+			while (changed) {
+				// Find the index of the next set bit.
+				uint32_t i = (uint32_t)__builtin_ctzll(changed);
+				// Clear the set bit.
+				changed &= (changed - 1);
+				input->joy_axis(id, (JoyAxis)i, joypad.axis_value[i]);
 			}
 
 			if (joypad.force_feedback) {
-				Input *input = Input::get_singleton();
 				uint64_t timestamp = input->get_joy_vibration_timestamp(id);
 
 				if (timestamp > (unsigned)joypad.ff_effect_timestamp) {

+ 2 - 1
editor/debugger/script_editor_debugger.cpp

@@ -408,6 +408,8 @@ void ScriptEditorDebugger::_msg_debug_exit(uint64_t p_thread_id, const Array &p_
 void ScriptEditorDebugger::_msg_set_pid(uint64_t p_thread_id, const Array &p_data) {
 	ERR_FAIL_COND(p_data.is_empty());
 	remote_pid = p_data[0];
+	// We emit the started signal after we've set the PID.
+	emit_signal(SNAME("started"));
 }
 
 void ScriptEditorDebugger::_msg_scene_click_ctrl(uint64_t p_thread_id, const Array &p_data) {
@@ -1174,7 +1176,6 @@ void ScriptEditorDebugger::start(Ref<RemoteDebuggerPeer> p_peer) {
 
 	_set_reason_text(TTR("Debug session started."), MESSAGE_SUCCESS);
 	_update_buttons_state();
-	emit_signal(SNAME("started"));
 
 	Array quit_keys = DebuggerMarshalls::serialize_key_shortcut(ED_GET_SHORTCUT("editor/stop_running_project"));
 	_put_msg("scene:setup_scene", quit_keys);

+ 9 - 1
editor/editor_node.cpp

@@ -7097,6 +7097,14 @@ void EditorNode::_touch_actions_panel_mode_changed() {
 }
 #endif
 
+#ifdef MACOS_ENABLED
+extern "C" GameViewPluginBase *get_game_view_plugin();
+#else
+GameViewPluginBase *get_game_view_plugin() {
+	return memnew(GameViewPlugin);
+}
+#endif
+
 EditorNode::EditorNode() {
 	DEV_ASSERT(!singleton);
 	singleton = this;
@@ -8175,7 +8183,7 @@ EditorNode::EditorNode() {
 	add_editor_plugin(memnew(ScriptEditorPlugin));
 
 	if (!Engine::get_singleton()->is_recovery_mode_hint()) {
-		add_editor_plugin(memnew(GameViewPlugin));
+		add_editor_plugin(get_game_view_plugin());
 	}
 
 	EditorAudioBuses *audio_bus_editor = EditorAudioBuses::register_editor();

+ 81 - 66
editor/plugins/embedded_process.cpp

@@ -35,25 +35,14 @@
 #include "scene/resources/style_box_flat.h"
 #include "scene/theme/theme_db.h"
 
-void EmbeddedProcess::_notification(int p_what) {
+void EmbeddedProcessBase::_notification(int p_what) {
 	switch (p_what) {
 		case NOTIFICATION_ENTER_TREE: {
 			window = get_window();
 		} break;
-		case NOTIFICATION_PROCESS: {
-			if (updated_embedded_process_queued) {
-				updated_embedded_process_queued = false;
-				_update_embedded_process();
-			}
-		} break;
 		case NOTIFICATION_DRAW: {
 			_draw();
 		} break;
-		case NOTIFICATION_RESIZED:
-		case NOTIFICATION_VISIBILITY_CHANGED:
-		case NOTIFICATION_WM_POSITION_CHANGED: {
-			queue_update_embedded_process();
-		} break;
 		case NOTIFICATION_THEME_CHANGED: {
 			focus_style_box = get_theme_stylebox(SNAME("FocusViewport"), EditorStringName(EditorStyles));
 			Ref<StyleBoxFlat> focus_style_box_flat = focus_style_box;
@@ -68,60 +57,43 @@ void EmbeddedProcess::_notification(int p_what) {
 				margin_bottom_right = Point2i();
 			}
 		} break;
-		case NOTIFICATION_FOCUS_ENTER: {
-			queue_update_embedded_process();
-		} break;
-		case NOTIFICATION_APPLICATION_FOCUS_IN: {
-			application_has_focus = true;
-			last_application_focus_time = OS::get_singleton()->get_ticks_msec();
-		} break;
-		case NOTIFICATION_APPLICATION_FOCUS_OUT: {
-			application_has_focus = false;
-		} break;
 	}
 }
 
-void EmbeddedProcess::set_window_size(const Size2i p_window_size) {
+void EmbeddedProcessBase::_bind_methods() {
+	ADD_SIGNAL(MethodInfo("embedding_completed"));
+	ADD_SIGNAL(MethodInfo("embedding_failed"));
+	ADD_SIGNAL(MethodInfo("embedded_process_updated"));
+	ADD_SIGNAL(MethodInfo("embedded_process_focused"));
+}
+
+void EmbeddedProcessBase::_draw() {
+	if (is_process_focused() && focus_style_box.is_valid()) {
+		Size2 size = get_size();
+		Rect2 r = Rect2(Point2(), size);
+		focus_style_box->draw(get_canvas_item(), r);
+	}
+}
+
+void EmbeddedProcessBase::set_window_size(const Size2i &p_window_size) {
 	if (window_size != p_window_size) {
 		window_size = p_window_size;
 		queue_update_embedded_process();
 	}
 }
 
-void EmbeddedProcess::set_keep_aspect(bool p_keep_aspect) {
+void EmbeddedProcessBase::set_keep_aspect(bool p_keep_aspect) {
 	if (keep_aspect != p_keep_aspect) {
 		keep_aspect = p_keep_aspect;
 		queue_update_embedded_process();
 	}
 }
 
-Rect2i EmbeddedProcess::get_adjusted_embedded_window_rect(Rect2i p_rect) {
-	Rect2i control_rect = Rect2i(p_rect.position + margin_top_left, (p_rect.size - get_margins_size()).maxi(1));
-	if (window) {
-		control_rect.position += window->get_position();
-	}
-	if (window_size != Size2i()) {
-		Rect2i desired_rect = Rect2i();
-		if (!keep_aspect && control_rect.size.x >= window_size.x && control_rect.size.y >= window_size.y) {
-			// Fixed at the desired size.
-			desired_rect.size = window_size;
-		} else {
-			float ratio = MIN((float)control_rect.size.x / window_size.x, (float)control_rect.size.y / window_size.y);
-			desired_rect.size = Size2i(window_size.x * ratio, window_size.y * ratio).maxi(1);
-		}
-		desired_rect.position = Size2i(control_rect.position.x + ((control_rect.size.x - desired_rect.size.x) / 2), control_rect.position.y + ((control_rect.size.y - desired_rect.size.y) / 2));
-		return desired_rect;
-	} else {
-		// Stretch, use all the control area.
-		return control_rect;
-	}
-}
-
-Rect2i EmbeddedProcess::get_screen_embedded_window_rect() {
+Rect2i EmbeddedProcessBase::get_screen_embedded_window_rect() const {
 	return get_adjusted_embedded_window_rect(get_global_rect());
 }
 
-int EmbeddedProcess::get_margin_size(Side p_side) const {
+int EmbeddedProcessBase::get_margin_size(Side p_side) const {
 	ERR_FAIL_INDEX_V((int)p_side, 4, 0);
 
 	switch (p_side) {
@@ -138,18 +110,51 @@ int EmbeddedProcess::get_margin_size(Side p_side) const {
 	return 0;
 }
 
-Size2 EmbeddedProcess::get_margins_size() {
+Size2 EmbeddedProcessBase::get_margins_size() const {
 	return margin_top_left + margin_bottom_right;
 }
 
-bool EmbeddedProcess::is_embedding_in_progress() {
+EmbeddedProcessBase::EmbeddedProcessBase() {
+	set_focus_mode(FOCUS_ALL);
+}
+
+EmbeddedProcessBase::~EmbeddedProcessBase() {
+}
+
+Rect2i EmbeddedProcess::get_adjusted_embedded_window_rect(const Rect2i &p_rect) const {
+	Rect2i control_rect = Rect2i(p_rect.position + margin_top_left, (p_rect.size - get_margins_size()).maxi(1));
+	if (window) {
+		control_rect.position += window->get_position();
+	}
+	if (window_size != Size2i()) {
+		Rect2i desired_rect;
+		if (!keep_aspect && control_rect.size.x >= window_size.x && control_rect.size.y >= window_size.y) {
+			// Fixed at the desired size.
+			desired_rect.size = window_size;
+		} else {
+			float ratio = MIN((float)control_rect.size.x / window_size.x, (float)control_rect.size.y / window_size.y);
+			desired_rect.size = Size2i(window_size.x * ratio, window_size.y * ratio).maxi(1);
+		}
+		desired_rect.position = Size2i(control_rect.position.x + ((control_rect.size.x - desired_rect.size.x) / 2), control_rect.position.y + ((control_rect.size.y - desired_rect.size.y) / 2));
+		return desired_rect;
+	} else {
+		// Stretch, use all the control area.
+		return control_rect;
+	}
+}
+
+bool EmbeddedProcess::is_embedding_in_progress() const {
 	return !timer_embedding->is_stopped();
 }
 
-bool EmbeddedProcess::is_embedding_completed() {
+bool EmbeddedProcess::is_embedding_completed() const {
 	return embedding_completed;
 }
 
+bool EmbeddedProcess::is_process_focused() const {
+	return focused_process_id == current_process_id && has_focus();
+}
+
 int EmbeddedProcess::get_embedded_pid() const {
 	return current_process_id;
 }
@@ -270,11 +275,29 @@ void EmbeddedProcess::_timer_embedding_timeout() {
 	_try_embed_process();
 }
 
-void EmbeddedProcess::_draw() {
-	if (focused_process_id == current_process_id && has_focus() && focus_style_box.is_valid()) {
-		Size2 size = get_size();
-		Rect2 r = Rect2(Point2(), size);
-		focus_style_box->draw(get_canvas_item(), r);
+void EmbeddedProcess::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_PROCESS: {
+			if (updated_embedded_process_queued) {
+				updated_embedded_process_queued = false;
+				_update_embedded_process();
+			}
+		} break;
+		case NOTIFICATION_RESIZED:
+		case NOTIFICATION_VISIBILITY_CHANGED:
+		case NOTIFICATION_WM_POSITION_CHANGED: {
+			queue_update_embedded_process();
+		} break;
+		case NOTIFICATION_APPLICATION_FOCUS_IN: {
+			application_has_focus = true;
+			last_application_focus_time = OS::get_singleton()->get_ticks_msec();
+		} break;
+		case NOTIFICATION_APPLICATION_FOCUS_OUT: {
+			application_has_focus = false;
+		} break;
+		case NOTIFICATION_FOCUS_ENTER: {
+			queue_update_embedded_process();
+		} break;
 	}
 }
 
@@ -386,14 +409,8 @@ Window *EmbeddedProcess::_get_current_modal_window() {
 	return nullptr;
 }
 
-void EmbeddedProcess::_bind_methods() {
-	ADD_SIGNAL(MethodInfo("embedding_completed"));
-	ADD_SIGNAL(MethodInfo("embedding_failed"));
-	ADD_SIGNAL(MethodInfo("embedded_process_updated"));
-	ADD_SIGNAL(MethodInfo("embedded_process_focused"));
-}
-
-EmbeddedProcess::EmbeddedProcess() {
+EmbeddedProcess::EmbeddedProcess() :
+		EmbeddedProcessBase() {
 	timer_embedding = memnew(Timer);
 	timer_embedding->set_wait_time(0.1);
 	timer_embedding->set_one_shot(true);
@@ -404,8 +421,6 @@ EmbeddedProcess::EmbeddedProcess() {
 	timer_update_embedded_process->set_wait_time(0.1);
 	add_child(timer_update_embedded_process);
 	timer_update_embedded_process->connect("timeout", callable_mp(this, &EmbeddedProcess::_timer_update_embedded_process_timeout));
-
-	set_focus_mode(FOCUS_ALL);
 }
 
 EmbeddedProcess::~EmbeddedProcess() {

+ 53 - 26
editor/plugins/embedded_process.h

@@ -32,8 +32,49 @@
 
 #include "scene/gui/control.h"
 
-class EmbeddedProcess : public Control {
-	GDCLASS(EmbeddedProcess, Control);
+class ScriptEditorDebugger;
+
+class EmbeddedProcessBase : public Control {
+	GDCLASS(EmbeddedProcessBase, Control);
+
+	void _draw();
+
+protected:
+	Ref<StyleBox> focus_style_box;
+	Size2i window_size;
+	bool keep_aspect = false;
+	Point2i margin_top_left;
+	Point2i margin_bottom_right;
+	Window *window = nullptr;
+
+	static void _bind_methods();
+	void _notification(int p_what);
+
+public:
+	virtual void set_script_debugger(ScriptEditorDebugger *p_debugger) {}
+
+	virtual bool is_embedding_completed() const = 0;
+	virtual bool is_embedding_in_progress() const = 0;
+	virtual bool is_process_focused() const = 0;
+	virtual void embed_process(OS::ProcessID p_pid) = 0;
+	virtual int get_embedded_pid() const = 0;
+	virtual void reset() = 0;
+	virtual void request_close() = 0;
+	virtual void queue_update_embedded_process() = 0;
+
+	void set_window_size(const Size2i &p_window_size);
+	void set_keep_aspect(bool p_keep_aspect);
+	virtual Rect2i get_adjusted_embedded_window_rect(const Rect2i &p_rect) const = 0;
+	Rect2i get_screen_embedded_window_rect() const;
+	int get_margin_size(Side p_side) const;
+	Size2 get_margins_size() const;
+
+	EmbeddedProcessBase();
+	virtual ~EmbeddedProcessBase();
+};
+
+class EmbeddedProcess : public EmbeddedProcessBase {
+	GDCLASS(EmbeddedProcess, EmbeddedProcessBase);
 
 	bool application_has_focus = true;
 	uint64_t last_application_focus_time = 0;
@@ -45,51 +86,37 @@ class EmbeddedProcess : public Control {
 	bool updated_embedded_process_queued = false;
 	bool last_updated_embedded_process_focused = false;
 
-	Window *window = nullptr;
 	Timer *timer_embedding = nullptr;
 	Timer *timer_update_embedded_process = nullptr;
 
 	const int embedding_timeout = 45000;
 
-	bool keep_aspect = false;
-	Size2i window_size;
-	Ref<StyleBox> focus_style_box;
-	Point2i margin_top_left;
-	Point2i margin_bottom_right;
 	Rect2i last_global_rect;
 
 	void _try_embed_process();
 	void _update_embedded_process();
 	void _timer_embedding_timeout();
 	void _timer_update_embedded_process_timeout();
-	void _draw();
 	void _check_mouse_over();
 	void _check_focused_process_id();
 	bool _is_embedded_process_updatable();
-	Rect2i _get_global_embedded_window_rect();
 	Window *_get_current_modal_window();
 
 protected:
-	static void _bind_methods();
 	void _notification(int p_what);
 
 public:
-	void embed_process(OS::ProcessID p_pid);
-	void reset();
-	void request_close();
-
-	void set_window_size(const Size2i p_window_size);
-	void set_keep_aspect(bool p_keep_aspect);
-	void queue_update_embedded_process();
+	bool is_embedding_in_progress() const override;
+	bool is_embedding_completed() const override;
+	bool is_process_focused() const override;
+	void embed_process(OS::ProcessID p_pid) override;
+	int get_embedded_pid() const override;
+	void reset() override;
+	void request_close() override;
+	void queue_update_embedded_process() override;
 
-	Rect2i get_adjusted_embedded_window_rect(Rect2i p_rect);
-	Rect2i get_screen_embedded_window_rect();
-	int get_margin_size(Side p_side) const;
-	Size2 get_margins_size();
-	bool is_embedding_in_progress();
-	bool is_embedding_completed();
-	int get_embedded_pid() const;
+	Rect2i get_adjusted_embedded_window_rect(const Rect2i &p_rect) const override;
 
 	EmbeddedProcess();
-	~EmbeddedProcess();
+	~EmbeddedProcess() override;
 };

+ 67 - 31
editor/plugins/game_view_plugin.cpp

@@ -243,11 +243,17 @@ void GameView::_sessions_changed() {
 
 	_update_debugger_buttons();
 
+#ifdef MACOS_ENABLED
+	if (!embedded_script_debugger || !embedded_script_debugger->is_session_active() || embedded_script_debugger->get_remote_pid() != embedded_process->get_embedded_pid()) {
+		_attach_script_debugger();
+	}
+#else
 	if (embedded_process->is_embedding_completed()) {
 		if (!embedded_script_debugger || !embedded_script_debugger->is_session_active() || embedded_script_debugger->get_remote_pid() != embedded_process->get_embedded_pid()) {
 			_attach_script_debugger();
 		}
 	}
+#endif
 }
 
 void GameView::_instance_starting_static(int p_idx, List<String> &r_arguments) {
@@ -370,7 +376,9 @@ void GameView::_stop_pressed() {
 }
 
 void GameView::_embedding_completed() {
+#ifndef MACOS_ENABLED
 	_attach_script_debugger();
+#endif
 	_update_ui();
 	if (make_floating_on_play) {
 		get_window()->set_flag(Window::FLAG_ALWAYS_ON_TOP, bool(GLOBAL_GET("display/window/size/always_on_top")));
@@ -507,9 +515,11 @@ GameView::EmbedAvailability GameView::_get_embed_available() {
 	if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_WINDOW_EMBEDDING)) {
 		return EMBED_NOT_AVAILABLE_FEATURE_NOT_SUPPORTED;
 	}
+#ifndef MACOS_ENABLED
 	if (get_tree()->get_root()->is_embedding_subwindows()) {
 		return EMBED_NOT_AVAILABLE_SINGLE_WINDOW_MODE;
 	}
+#endif
 	String display_driver = GLOBAL_GET("display/display_server/driver");
 	if (display_driver == "headless" || display_driver == "wayland") {
 		return EMBED_NOT_AVAILABLE_PROJECT_DISPLAY_DRIVER;
@@ -786,14 +796,19 @@ void GameView::_attach_script_debugger() {
 	}
 
 	embedded_script_debugger = nullptr;
-	for (int i = 0; EditorDebuggerNode::get_singleton()->get_debugger(i); i++) {
-		ScriptEditorDebugger *script_debugger = EditorDebuggerNode::get_singleton()->get_debugger(i);
+	int i = 0;
+	while (ScriptEditorDebugger *script_debugger = EditorDebuggerNode::get_singleton()->get_debugger(i)) {
 		if (script_debugger->is_session_active() && script_debugger->get_remote_pid() == embedded_process->get_embedded_pid()) {
 			embedded_script_debugger = script_debugger;
 			break;
 		}
+		i++;
 	}
 
+#ifdef MACOS_ENABLED
+	embedded_process->set_script_debugger(embedded_script_debugger);
+#endif
+
 	if (embedded_script_debugger) {
 		embedded_script_debugger->connect("remote_window_title_changed", callable_mp(this, &GameView::_remote_window_title_changed));
 		embedded_script_debugger->connect("embed_shortcut_requested", callable_mp(this, &GameView::_handle_shortcut_requested));
@@ -845,6 +860,12 @@ void GameView::_update_arguments_for_instance(int p_idx, List<String> &r_argumen
 	List<String>::Element *N = r_arguments.insert_before(user_args_element, "--wid");
 	N = r_arguments.insert_after(N, itos(DisplayServer::get_singleton()->window_get_native_handle(DisplayServer::WINDOW_HANDLE, get_window()->get_window_id())));
 
+#if MACOS_ENABLED
+	r_arguments.push_back("--display-driver");
+	r_arguments.push_back("embedded");
+	r_arguments.push_back("--embedded");
+#endif
+
 	// Be sure to have the correct window size in the embedded_process control.
 	_update_embed_window_size();
 	Rect2i rect = embedded_process->get_screen_embedded_window_rect();
@@ -931,11 +952,12 @@ void GameView::_feature_profile_changed() {
 	node_type_button[RuntimeNodeSelect::NODE_TYPE_3D]->set_visible(is_3d_enabled);
 }
 
-GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
+GameView::GameView(Ref<GameViewDebugger> p_debugger, EmbeddedProcessBase *p_embedded_process, WindowWrapper *p_wrapper) {
 	singleton = this;
 
 	debugger = p_debugger;
 	window_wrapper = p_wrapper;
+	embedded_process = p_embedded_process;
 
 	// Add some margin to the sides for better aesthetics.
 	// This prevents the first button's hover/pressed effect from "touching" the panel's border,
@@ -1051,7 +1073,6 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 	camera_override_menu->set_h_size_flags(SIZE_SHRINK_END);
 	camera_override_menu->set_tooltip_text(TTR("Camera Override Options"));
 	camera_override_menu->set_accessibility_name(TTRC("Camera Override Options"));
-	_camera_override_menu_id_pressed(EditorSettings::get_singleton()->get_project_metadata("game_view", "camera_override_mode", 0));
 
 	PopupMenu *menu = camera_override_menu->get_popup();
 	menu->connect(SceneStringName(id_pressed), callable_mp(this, &GameView::_camera_override_menu_id_pressed));
@@ -1061,6 +1082,7 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 	menu->add_radio_check_item(TTR("Manipulate In-Game"), CAMERA_MODE_INGAME);
 	menu->set_item_checked(menu->get_item_index(CAMERA_MODE_INGAME), true);
 	menu->add_radio_check_item(TTR("Manipulate From Editors"), CAMERA_MODE_EDITORS);
+	_camera_override_menu_id_pressed(EditorSettings::get_singleton()->get_project_metadata("game_view", "camera_override_mode", 0));
 
 	embedding_separator = memnew(VSeparator);
 	main_menu_hbox->add_child(embedding_separator);
@@ -1118,7 +1140,6 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 	panel->set_theme_type_variation("GamePanel");
 	panel->set_v_size_flags(SIZE_EXPAND_FILL);
 
-	embedded_process = memnew(EmbeddedProcess);
 	panel->add_child(embedded_process);
 	embedded_process->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
 	embedded_process->connect("embedding_failed", callable_mp(this, &GameView::_embedding_failed));
@@ -1131,6 +1152,9 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 	state_container->add_theme_constant_override("margin_left", 8 * EDSCALE);
 	state_container->add_theme_constant_override("margin_right", 8 * EDSCALE);
 	state_container->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
+#ifdef MACOS_ENABLED
+	state_container->set_mouse_filter(Control::MOUSE_FILTER_IGNORE);
+#endif
 	panel->add_child(state_container);
 
 	state_label = memnew(Label());
@@ -1156,7 +1180,7 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 
 ///////
 
-void GameViewPlugin::selected_notify() {
+void GameViewPluginBase::selected_notify() {
 	if (_is_window_wrapper_enabled()) {
 #ifdef ANDROID_ENABLED
 		notify_main_screen_changed(get_plugin_name());
@@ -1168,7 +1192,7 @@ void GameViewPlugin::selected_notify() {
 }
 
 #ifndef ANDROID_ENABLED
-void GameViewPlugin::make_visible(bool p_visible) {
+void GameViewPluginBase::make_visible(bool p_visible) {
 	if (p_visible) {
 		window_wrapper->show();
 	} else {
@@ -1176,35 +1200,54 @@ void GameViewPlugin::make_visible(bool p_visible) {
 	}
 }
 
-void GameViewPlugin::set_window_layout(Ref<ConfigFile> p_layout) {
+void GameViewPluginBase::set_window_layout(Ref<ConfigFile> p_layout) {
 	game_view->set_window_layout(p_layout);
 }
 
-void GameViewPlugin::get_window_layout(Ref<ConfigFile> p_layout) {
+void GameViewPluginBase::get_window_layout(Ref<ConfigFile> p_layout) {
 	game_view->get_window_layout(p_layout);
 }
+
+void GameViewPluginBase::setup(Ref<GameViewDebugger> p_debugger, EmbeddedProcessBase *p_embedded_process) {
+	debugger = p_debugger;
+
+	window_wrapper = memnew(WindowWrapper);
+	window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Game Workspace")));
+	window_wrapper->set_margins_enabled(true);
+
+	game_view = memnew(GameView(debugger, p_embedded_process, window_wrapper));
+	game_view->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+
+	window_wrapper->set_wrapped_control(game_view, nullptr);
+
+	EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(window_wrapper);
+	window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
+	window_wrapper->hide();
+	window_wrapper->connect("window_visibility_changed", callable_mp(this, &GameViewPlugin::_focus_another_editor).unbind(1));
+}
+
 #endif // ANDROID_ENABLED
 
-void GameViewPlugin::_notification(int p_what) {
+void GameViewPluginBase::_notification(int p_what) {
 	switch (p_what) {
 		case NOTIFICATION_ENTER_TREE: {
 			add_debugger_plugin(debugger);
-			connect("main_screen_changed", callable_mp(this, &GameViewPlugin::_save_last_editor));
+			connect("main_screen_changed", callable_mp(this, &GameViewPluginBase::_save_last_editor));
 		} break;
 		case NOTIFICATION_EXIT_TREE: {
 			remove_debugger_plugin(debugger);
-			disconnect("main_screen_changed", callable_mp(this, &GameViewPlugin::_save_last_editor));
+			disconnect("main_screen_changed", callable_mp(this, &GameViewPluginBase::_save_last_editor));
 		} break;
 	}
 }
 
-void GameViewPlugin::_save_last_editor(const String &p_editor) {
+void GameViewPluginBase::_save_last_editor(const String &p_editor) {
 	if (p_editor != get_plugin_name()) {
 		last_editor = p_editor;
 	}
 }
 
-void GameViewPlugin::_focus_another_editor() {
+void GameViewPluginBase::_focus_another_editor() {
 	if (_is_window_wrapper_enabled()) {
 		if (last_editor.is_empty()) {
 			EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_2D);
@@ -1214,7 +1257,7 @@ void GameViewPlugin::_focus_another_editor() {
 	}
 }
 
-bool GameViewPlugin::_is_window_wrapper_enabled() const {
+bool GameViewPluginBase::_is_window_wrapper_enabled() const {
 #ifdef ANDROID_ENABLED
 	return true;
 #else
@@ -1222,22 +1265,15 @@ bool GameViewPlugin::_is_window_wrapper_enabled() const {
 #endif // ANDROID_ENABLED
 }
 
-GameViewPlugin::GameViewPlugin() {
-	debugger.instantiate();
+GameViewPluginBase::GameViewPluginBase() {
+}
 
+GameViewPlugin::GameViewPlugin() :
+		GameViewPluginBase() {
 #ifndef ANDROID_ENABLED
-	window_wrapper = memnew(WindowWrapper);
-	window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Game Workspace")));
-	window_wrapper->set_margins_enabled(true);
-
-	game_view = memnew(GameView(debugger, window_wrapper));
-	game_view->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-
-	window_wrapper->set_wrapped_control(game_view, nullptr);
-
-	EditorNode::get_singleton()->get_editor_main_screen()->get_control()->add_child(window_wrapper);
-	window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
-	window_wrapper->hide();
-	window_wrapper->connect("window_visibility_changed", callable_mp(this, &GameViewPlugin::_focus_another_editor).unbind(1));
-#endif // ANDROID_ENABLED
+	Ref<GameViewDebugger> game_view_debugger;
+	game_view_debugger.instantiate();
+	EmbeddedProcess *embedded_process = memnew(EmbeddedProcess);
+	setup(game_view_debugger, embedded_process);
+#endif
 }

+ 14 - 5
editor/plugins/game_view_plugin.h

@@ -37,7 +37,7 @@
 #include "scene/debugger/scene_debugger.h"
 #include "scene/gui/box_container.h"
 
-class EmbeddedProcess;
+class EmbeddedProcessBase;
 class VSeparator;
 class WindowWrapper;
 class ScriptEditorDebugger;
@@ -154,7 +154,7 @@ class GameView : public VBoxContainer {
 	MenuButton *embed_options_menu = nullptr;
 	Label *game_size_label = nullptr;
 	Panel *panel = nullptr;
-	EmbeddedProcess *embedded_process = nullptr;
+	EmbeddedProcessBase *embedded_process = nullptr;
 	Label *state_label = nullptr;
 
 	void _sessions_changed();
@@ -214,11 +214,11 @@ public:
 	void set_window_layout(Ref<ConfigFile> p_layout);
 	void get_window_layout(Ref<ConfigFile> p_layout);
 
-	GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper);
+	GameView(Ref<GameViewDebugger> p_debugger, EmbeddedProcessBase *p_embedded_process, WindowWrapper *p_wrapper);
 };
 
-class GameViewPlugin : public EditorPlugin {
-	GDCLASS(GameViewPlugin, EditorPlugin);
+class GameViewPluginBase : public EditorPlugin {
+	GDCLASS(GameViewPluginBase, EditorPlugin);
 
 #ifndef ANDROID_ENABLED
 	GameView *game_view = nullptr;
@@ -238,6 +238,9 @@ class GameViewPlugin : public EditorPlugin {
 
 protected:
 	void _notification(int p_what);
+#ifndef ANDROID_ENABLED
+	void setup(Ref<GameViewDebugger> p_debugger, EmbeddedProcessBase *p_embedded_process);
+#endif
 
 public:
 	virtual String get_plugin_name() const override { return TTRC("Game"); }
@@ -254,6 +257,12 @@ public:
 	virtual void set_window_layout(Ref<ConfigFile> p_layout) override;
 	virtual void get_window_layout(Ref<ConfigFile> p_layout) override;
 #endif // ANDROID_ENABLED
+	GameViewPluginBase();
+};
 
+class GameViewPlugin : public GameViewPluginBase {
+	GDCLASS(GameViewPlugin, GameViewPluginBase);
+
+public:
 	GameViewPlugin();
 };

+ 9 - 0
platform/macos/SCsub

@@ -11,7 +11,10 @@ files = [
     "godot_application_delegate.mm",
     "crash_handler_macos.mm",
     "macos_terminal_logger.mm",
+    "display_server_embedded.mm",
     "display_server_macos.mm",
+    "embedded_debugger.mm",
+    "embedded_gl_manager.mm",
     "godot_button_view.mm",
     "godot_content_view.mm",
     "godot_status_item.mm",
@@ -30,6 +33,12 @@ files = [
     "gl_manager_macos_legacy.mm",
 ]
 
+if env.editor_build:
+    files += [
+        "editor/embedded_game_view_plugin.mm",
+        "editor/embedded_process_macos.mm",
+    ]
+
 prog = env.add_program("#bin/godot", files)
 
 if env["debug_symbols"] and env["separate_debug_symbols"]:

+ 2 - 2
platform/macos/detect.py

@@ -231,6 +231,8 @@ def configure(env: "SConsEnvironment"):
             "Security",
             "-framework",
             "UniformTypeIdentifiers",
+            "-framework",
+            "IOSurface",
         ]
     )
     env.Append(LIBS=["pthread", "z"])
@@ -245,7 +247,6 @@ def configure(env: "SConsEnvironment"):
             env.Append(LINKFLAGS=["-lANGLE.macos." + env["arch"]])
             env.Append(LINKFLAGS=["-lEGL.macos." + env["arch"]])
             env.Append(LINKFLAGS=["-lGLES.macos." + env["arch"]])
-            extra_frameworks.add("IOSurface")
         env.Prepend(CPPEXTPATH=["#thirdparty/angle/include"])
 
     env.Append(LINKFLAGS=["-rpath", "@executable_path/../Frameworks", "-rpath", "@executable_path"])
@@ -264,7 +265,6 @@ def configure(env: "SConsEnvironment"):
     if env["vulkan"]:
         env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"])
         extra_frameworks.add("Metal")
-        extra_frameworks.add("IOSurface")
         if not env["use_volk"]:
             env.Append(LINKFLAGS=["-lMoltenVK"])
 

+ 231 - 0
platform/macos/display_server_embedded.h

@@ -0,0 +1,231 @@
+/**************************************************************************/
+/*  display_server_embedded.h                                             */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "core/input/input.h"
+#include "servers/display_server.h"
+
+#if defined(GLES3_ENABLED)
+#include "embedded_gl_manager.h"
+#include "platform_gl.h"
+#endif // GLES3_ENABLED
+
+#if defined(RD_ENABLED)
+#include "servers/rendering/rendering_device.h"
+
+#if defined(VULKAN_ENABLED)
+#import "rendering_context_driver_vulkan_macos.h"
+#endif // VULKAN_ENABLED
+#if defined(METAL_ENABLED)
+#import "drivers/metal/rendering_context_driver_metal.h"
+#endif
+#endif // RD_ENABLED
+
+@class CAContext;
+
+class DisplayServerEmbedded : public DisplayServer {
+	GDCLASS(DisplayServerEmbedded, DisplayServer)
+
+	_THREAD_SAFE_CLASS_
+
+	struct {
+		float screen_max_scale = 1.0f;
+		float screen_dpi = 96.0f;
+	} state;
+
+	NativeMenu *native_menu = nullptr;
+
+	HashMap<WindowID, ObjectID> window_attached_instance_id;
+
+	HashMap<WindowID, Callable> window_event_callbacks;
+	HashMap<WindowID, Callable> window_resize_callbacks;
+	HashMap<WindowID, Callable> input_event_callbacks;
+	HashMap<WindowID, Callable> input_text_callbacks;
+
+	float content_scale = 1.0f;
+
+	WindowID window_id_counter = MAIN_WINDOW_ID;
+
+	CAContext *ca_context = nil;
+	// Either be a CAMetalLayer or a CALayer depending on the rendering driver.
+	CALayer *layer = nil;
+#ifdef GLES3_ENABLED
+	GLManagerEmbedded *gl_manager = nullptr;
+#endif
+
+#if defined(RD_ENABLED)
+	RenderingContextDriver *rendering_context = nullptr;
+	RenderingDevice *rendering_device = nullptr;
+#endif
+
+	String rendering_driver;
+
+	Point2i ime_last_position;
+	Point2i im_selection;
+	String im_text;
+
+	MouseMode mouse_mode = MOUSE_MODE_VISIBLE;
+	MouseMode mouse_mode_base = MOUSE_MODE_VISIBLE;
+	MouseMode mouse_mode_override = MOUSE_MODE_VISIBLE;
+	bool mouse_mode_override_enabled = false;
+	void _mouse_update_mode();
+
+	CursorShape cursor_shape = CURSOR_ARROW;
+
+	struct Joy {
+		String name;
+		uint64_t timestamp = 0;
+
+		Joy() = default;
+		Joy(const String &p_name) :
+				name(p_name) {}
+	};
+	HashMap<int, Joy> joysticks;
+
+public:
+	static void register_embedded_driver();
+	static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t p_parent_window, Error &r_error);
+	static Vector<String> get_rendering_drivers_func();
+
+	// MARK: - Events
+
+	virtual void process_events() override;
+
+	virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override;
+
+	static void _dispatch_input_events(const Ref<InputEvent> &p_event);
+	void send_input_event(const Ref<InputEvent> &p_event, DisplayServer::WindowID p_id = MAIN_WINDOW_ID) const;
+	void send_input_text(const String &p_text, DisplayServer::WindowID p_id = MAIN_WINDOW_ID) const;
+	void send_window_event(DisplayServer::WindowEvent p_event, DisplayServer::WindowID p_id = MAIN_WINDOW_ID) const;
+	void _window_callback(const Callable &p_callable, const Variant &p_arg) const;
+
+	virtual void beep() const override;
+
+	// MARK: - Mouse
+	virtual void mouse_set_mode(MouseMode p_mode) override;
+	virtual MouseMode mouse_get_mode() const override;
+	virtual void mouse_set_mode_override(MouseMode p_mode) override;
+	virtual MouseMode mouse_get_mode_override() const override;
+	virtual void mouse_set_mode_override_enabled(bool p_override_enabled) override;
+	virtual bool mouse_is_mode_override_enabled() const override;
+
+	virtual Point2i mouse_get_position() const override;
+	virtual BitField<MouseButtonMask> mouse_get_button_state() const override;
+
+	// MARK: - Joystick
+
+	void joy_add(int p_idx, const String &p_name);
+	void joy_del(int p_idx);
+
+	// MARK: - Window
+
+	virtual bool has_feature(Feature p_feature) const override;
+	virtual String get_name() const override;
+
+	virtual int get_screen_count() const override;
+	virtual int get_primary_screen() const override;
+	virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+	virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+	virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+	virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+	virtual float screen_get_refresh_rate(int p_screen = SCREEN_OF_MAIN_WINDOW) const override;
+
+	virtual Vector<DisplayServer::WindowID> get_window_list() const override;
+
+	virtual WindowID get_window_at_screen_position(const Point2i &p_position) const override;
+
+	virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override;
+
+	virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override;
+	virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override;
+
+	virtual Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override;
+	virtual Point2i window_get_position_with_decorations(WindowID p_window = MAIN_WINDOW_ID) const override;
+	virtual void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID) override;
+
+	virtual void window_set_transient(WindowID p_window, WindowID p_parent) override;
+
+	virtual void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override;
+	virtual Size2i window_get_size_with_decorations(WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual bool window_is_focused(WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual float screen_get_max_scale() const override;
+
+	virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override;
+
+	virtual bool can_any_window_draw() const override;
+
+	virtual void window_set_ime_active(const bool p_active, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual void window_set_ime_position(const Point2i &p_pos, WindowID p_window = MAIN_WINDOW_ID) override;
+
+	virtual void window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID) override;
+	virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override;
+
+	void update_im_text(const Point2i &p_selection, const String &p_text);
+	virtual Point2i ime_get_selection() const override;
+	virtual String ime_get_text() const override;
+
+	virtual void cursor_set_shape(CursorShape p_shape) override;
+	virtual CursorShape cursor_get_shape() const override;
+	virtual void cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override;
+
+	void update_state(const Dictionary &p_state);
+	void set_content_scale(float p_scale);
+	virtual void swap_buffers() override;
+
+	DisplayServerEmbedded(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error);
+	~DisplayServerEmbedded();
+};

+ 744 - 0
platform/macos/display_server_embedded.mm

@@ -0,0 +1,744 @@
+/**************************************************************************/
+/*  display_server_embedded.mm                                            */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#import "display_server_embedded.h"
+
+#import "embedded_debugger.h"
+#import "macos_quartz_core_spi.h"
+
+#import "core/config/project_settings.h"
+#import "core/debugger/engine_debugger.h"
+
+#if defined(GLES3_ENABLED)
+#include "drivers/gles3/rasterizer_gles3.h"
+#endif
+
+#if defined(RD_ENABLED)
+#import "servers/rendering/renderer_rd/renderer_compositor_rd.h"
+#endif
+
+DisplayServerEmbedded::DisplayServerEmbedded(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error) {
+	EmbeddedDebugger::initialize(this);
+
+	r_error = OK; // default to OK
+
+	native_menu = memnew(NativeMenu);
+
+	Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events);
+
+	rendering_driver = p_rendering_driver;
+
+#if defined(RD_ENABLED)
+#if defined(VULKAN_ENABLED)
+#if defined(__x86_64__)
+	bool fallback_to_vulkan = GLOBAL_GET("rendering/rendering_device/fallback_to_vulkan");
+	if (!fallback_to_vulkan) {
+		WARN_PRINT("Metal is not supported on Intel Macs, switching to Vulkan.");
+	}
+	// Metal rendering driver not available on Intel.
+	if (rendering_driver == "metal") {
+		rendering_driver = "vulkan";
+		OS::get_singleton()->set_current_rendering_driver_name(rendering_driver);
+	}
+#endif
+	if (rendering_driver == "vulkan") {
+		rendering_context = memnew(RenderingContextDriverVulkanMacOS);
+	}
+#endif
+#if defined(METAL_ENABLED)
+	if (rendering_driver == "metal") {
+		rendering_context = memnew(RenderingContextDriverMetal);
+	}
+#endif
+
+	if (rendering_context) {
+		if (rendering_context->initialize() != OK) {
+			memdelete(rendering_context);
+			rendering_context = nullptr;
+#if defined(GLES3_ENABLED)
+			bool fallback_to_opengl3 = GLOBAL_GET("rendering/rendering_device/fallback_to_opengl3");
+			if (fallback_to_opengl3 && rendering_driver != "opengl3") {
+				WARN_PRINT("Your device seem not to support MoltenVK or Metal, switching to OpenGL 3.");
+				rendering_driver = "opengl3";
+				OS::get_singleton()->set_current_rendering_method("gl_compatibility");
+				OS::get_singleton()->set_current_rendering_driver_name(rendering_driver);
+			} else
+#endif
+			{
+				r_error = ERR_CANT_CREATE;
+				ERR_FAIL_MSG("Could not initialize " + rendering_driver);
+			}
+		}
+	}
+#endif
+
+#if defined(GLES3_ENABLED)
+	if (rendering_driver == "opengl3_angle") {
+		WARN_PRINT("ANGLE not supported for embedded display, switching to native OpenGL.");
+		rendering_driver = "opengl3";
+		OS::get_singleton()->set_current_rendering_driver_name(rendering_driver);
+	}
+
+	if (rendering_driver == "opengl3") {
+		gl_manager = memnew(GLManagerEmbedded);
+		if (gl_manager->initialize() != OK) {
+			memdelete(gl_manager);
+			gl_manager = nullptr;
+			r_error = ERR_UNAVAILABLE;
+			ERR_FAIL_MSG("Could not initialize native OpenGL.");
+		}
+		layer = [CALayer new];
+		// OpenGL content is flipped, so it must be transformed.
+		layer.anchorPoint = CGPointMake(0, 0);
+		layer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0);
+
+		Error err = gl_manager->window_create(window_id_counter, layer, p_resolution.width, p_resolution.height);
+		if (err != OK) {
+			ERR_FAIL_MSG("Could not create OpenGL context.");
+		}
+	}
+#endif
+
+#if defined(RD_ENABLED)
+	if (rendering_context) {
+		layer = [CAMetalLayer new];
+		layer.anchorPoint = CGPointMake(0, 1);
+
+		union {
+#ifdef VULKAN_ENABLED
+			RenderingContextDriverVulkanMacOS::WindowPlatformData vulkan;
+#endif
+#ifdef METAL_ENABLED
+			RenderingContextDriverMetal::WindowPlatformData metal;
+#endif
+		} wpd;
+#ifdef VULKAN_ENABLED
+		if (rendering_driver == "vulkan") {
+			wpd.vulkan.layer_ptr = (CAMetalLayer *const *)&layer;
+		}
+#endif
+#ifdef METAL_ENABLED
+		if (rendering_driver == "metal") {
+			wpd.metal.layer = (CAMetalLayer *)layer;
+		}
+#endif
+		Error err = rendering_context->window_create(window_id_counter, &wpd);
+		ERR_FAIL_COND_MSG(err != OK, vformat("Can't create a %s context", rendering_driver));
+
+		// The rendering context is always in pixels
+		rendering_context->window_set_size(window_id_counter, p_resolution.width, p_resolution.height);
+		rendering_context->window_set_vsync_mode(window_id_counter, p_vsync_mode);
+	}
+#endif
+
+#if defined(GLES3_ENABLED)
+	if (rendering_driver == "opengl3") {
+		RasterizerGLES3::make_current(true);
+	}
+	if (rendering_driver == "opengl3_angle") {
+		RasterizerGLES3::make_current(false);
+	}
+#endif
+#if defined(RD_ENABLED)
+	if (rendering_context) {
+		rendering_device = memnew(RenderingDevice);
+		rendering_device->initialize(rendering_context, MAIN_WINDOW_ID);
+		rendering_device->screen_create(MAIN_WINDOW_ID);
+
+		RendererCompositorRD::make_current();
+	}
+#endif
+
+	constexpr CGFloat CONTENT_SCALE = 2.0;
+	layer.contentsScale = CONTENT_SCALE;
+	layer.magnificationFilter = kCAFilterNearest;
+	layer.minificationFilter = kCAFilterNearest;
+	layer.opaque = NO; // Never opaque when embedded.
+	layer.actions = @{ @"contents" : [NSNull null] }; // Disable implicit animations for contents.
+	// AppKit frames, bounds and positions are always in points.
+	CGRect bounds = CGRectMake(0, 0, p_resolution.width, p_resolution.height);
+	bounds = CGRectApplyAffineTransform(bounds, CGAffineTransformMakeScale(1.0 / CONTENT_SCALE, 1.0 / CONTENT_SCALE));
+	layer.bounds = bounds;
+
+	CGSConnectionID connection_id = CGSMainConnectionID();
+	ca_context = [CAContext contextWithCGSConnection:connection_id options:@{ kCAContextCIFilterBehavior : @"ignore" }];
+	ca_context.layer = layer;
+
+	{
+		Array arr = { ca_context.contextId };
+		EngineDebugger::get_singleton()->send_message("game_view:set_context_id", arr);
+	}
+}
+
+DisplayServerEmbedded::~DisplayServerEmbedded() {
+	if (native_menu) {
+		memdelete(native_menu);
+		native_menu = nullptr;
+	}
+
+	EmbeddedDebugger::deinitialize();
+
+#if defined(GLES3_ENABLED)
+	if (gl_manager) {
+		memdelete(gl_manager);
+		gl_manager = nullptr;
+	}
+#endif
+
+#if defined(RD_ENABLED)
+	if (rendering_device) {
+		memdelete(rendering_device);
+		rendering_device = nullptr;
+	}
+
+	if (rendering_context) {
+		memdelete(rendering_context);
+		rendering_context = nullptr;
+	}
+#endif
+}
+
+DisplayServer *DisplayServerEmbedded::create_func(const String &p_rendering_driver, WindowMode p_mode, DisplayServer::VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, int64_t /* p_parent_window */, Error &r_error) {
+	return memnew(DisplayServerEmbedded(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, p_context, r_error));
+}
+
+Vector<String> DisplayServerEmbedded::get_rendering_drivers_func() {
+	Vector<String> drivers;
+
+#if defined(VULKAN_ENABLED)
+	drivers.push_back("vulkan");
+#endif
+#if defined(METAL_ENABLED)
+	drivers.push_back("metal");
+#endif
+#if defined(GLES3_ENABLED)
+	drivers.push_back("opengl3");
+#endif
+
+	return drivers;
+}
+
+void DisplayServerEmbedded::register_embedded_driver() {
+	register_create_function("embedded", create_func, get_rendering_drivers_func);
+}
+
+void DisplayServerEmbedded::beep() const {
+	NSBeep();
+}
+
+// MARK: - Mouse
+
+void DisplayServerEmbedded::_mouse_update_mode() {
+	MouseMode wanted_mouse_mode = mouse_mode_override_enabled
+			? mouse_mode_override
+			: mouse_mode_base;
+
+	if (wanted_mouse_mode == mouse_mode) {
+		return;
+	}
+
+	EngineDebugger::get_singleton()->send_message("game_view:mouse_set_mode", { wanted_mouse_mode });
+
+	mouse_mode = wanted_mouse_mode;
+}
+
+void DisplayServerEmbedded::mouse_set_mode(MouseMode p_mode) {
+	if (p_mode == mouse_mode_base) {
+		return;
+	}
+	mouse_mode_base = p_mode;
+	_mouse_update_mode();
+}
+
+DisplayServerEmbedded::MouseMode DisplayServerEmbedded::mouse_get_mode() const {
+	return mouse_mode;
+}
+
+void DisplayServerEmbedded::mouse_set_mode_override(MouseMode p_mode) {
+	ERR_FAIL_INDEX(p_mode, MouseMode::MOUSE_MODE_MAX);
+	if (p_mode == mouse_mode_override) {
+		return;
+	}
+	mouse_mode_override = p_mode;
+	_mouse_update_mode();
+}
+
+DisplayServer::MouseMode DisplayServerEmbedded::mouse_get_mode_override() const {
+	return mouse_mode_override;
+}
+
+void DisplayServerEmbedded::mouse_set_mode_override_enabled(bool p_override_enabled) {
+	if (p_override_enabled == mouse_mode_override_enabled) {
+		return;
+	}
+	mouse_mode_override_enabled = p_override_enabled;
+	_mouse_update_mode();
+}
+
+bool DisplayServerEmbedded::mouse_is_mode_override_enabled() const {
+	return mouse_mode_override_enabled;
+}
+
+Point2i DisplayServerEmbedded::mouse_get_position() const {
+	_THREAD_SAFE_METHOD_
+
+	const NSPoint mouse_pos = [NSEvent mouseLocation];
+	const float scale = screen_get_max_scale();
+
+	for (NSScreen *screen in [NSScreen screens]) {
+		NSRect frame = [screen frame];
+		if (NSMouseInRect(mouse_pos, frame, NO)) {
+			Vector2i pos = Vector2i((int)mouse_pos.x, (int)mouse_pos.y);
+			pos *= scale;
+			// TODO(sgc): fix this
+			// pos -= _get_screens_origin();
+			pos.y *= -1;
+			return pos;
+		}
+	}
+	return Vector2i();
+}
+
+BitField<MouseButtonMask> DisplayServerEmbedded::mouse_get_button_state() const {
+	BitField<MouseButtonMask> last_button_state = MouseButtonMask::NONE;
+
+	NSUInteger buttons = [NSEvent pressedMouseButtons];
+	if (buttons & (1 << 0)) {
+		last_button_state.set_flag(MouseButtonMask::LEFT);
+	}
+	if (buttons & (1 << 1)) {
+		last_button_state.set_flag(MouseButtonMask::RIGHT);
+	}
+	if (buttons & (1 << 2)) {
+		last_button_state.set_flag(MouseButtonMask::MIDDLE);
+	}
+	if (buttons & (1 << 3)) {
+		last_button_state.set_flag(MouseButtonMask::MB_XBUTTON1);
+	}
+	if (buttons & (1 << 4)) {
+		last_button_state.set_flag(MouseButtonMask::MB_XBUTTON2);
+	}
+	return last_button_state;
+}
+
+// MARK: Events
+
+void DisplayServerEmbedded::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) {
+	window_resize_callbacks[p_window] = p_callable;
+}
+
+void DisplayServerEmbedded::window_set_window_event_callback(const Callable &p_callable, WindowID p_window) {
+	window_event_callbacks[p_window] = p_callable;
+}
+void DisplayServerEmbedded::window_set_input_event_callback(const Callable &p_callable, WindowID p_window) {
+	input_event_callbacks[p_window] = p_callable;
+}
+
+void DisplayServerEmbedded::window_set_input_text_callback(const Callable &p_callable, WindowID p_window) {
+	input_text_callbacks[p_window] = p_callable;
+}
+
+void DisplayServerEmbedded::window_set_drop_files_callback(const Callable &p_callable, WindowID p_window) {
+	// Not supported
+}
+
+void DisplayServerEmbedded::joy_add(int p_idx, const String &p_name) {
+	Joy *joy = joysticks.getptr(p_idx);
+	if (joy == nullptr) {
+		joysticks[p_idx] = Joy(p_name);
+		Input::get_singleton()->joy_connection_changed(p_idx, true, p_name);
+	}
+}
+
+void DisplayServerEmbedded::joy_del(int p_idx) {
+	if (joysticks.erase(p_idx)) {
+		Input::get_singleton()->joy_connection_changed(p_idx, false, String());
+	}
+}
+
+void DisplayServerEmbedded::process_events() {
+	Input *input = Input::get_singleton();
+	for (KeyValue<int, Joy> &kv : joysticks) {
+		uint64_t ts = input->get_joy_vibration_timestamp(kv.key);
+		if (ts > kv.value.timestamp) {
+			kv.value.timestamp = ts;
+			Vector2 strength = input->get_joy_vibration_strength(kv.key);
+			if (strength == Vector2()) {
+				EngineDebugger::get_singleton()->send_message("game_view:joy_stop", { kv.key });
+			} else {
+				float duration = input->get_joy_vibration_duration(kv.key);
+				EngineDebugger::get_singleton()->send_message("game_view:joy_start", { kv.key, duration, strength });
+			}
+		}
+	}
+
+	input->flush_buffered_events();
+}
+
+void DisplayServerEmbedded::_dispatch_input_events(const Ref<InputEvent> &p_event) {
+	Ref<InputEventFromWindow> event_from_window = p_event;
+	WindowID window_id = INVALID_WINDOW_ID;
+	if (event_from_window.is_valid()) {
+		window_id = event_from_window->get_window_id();
+	}
+	DisplayServerEmbedded *ds = (DisplayServerEmbedded *)DisplayServer::get_singleton();
+	ds->send_input_event(p_event, window_id);
+}
+
+void DisplayServerEmbedded::send_input_event(const Ref<InputEvent> &p_event, WindowID p_id) const {
+	if (p_id != INVALID_WINDOW_ID) {
+		_window_callback(input_event_callbacks[p_id], p_event);
+	} else {
+		for (const KeyValue<WindowID, Callable> &E : input_event_callbacks) {
+			_window_callback(E.value, p_event);
+		}
+	}
+}
+
+void DisplayServerEmbedded::send_input_text(const String &p_text, WindowID p_id) const {
+	const Callable *cb = input_text_callbacks.getptr(p_id);
+	if (cb) {
+		_window_callback(*cb, p_text);
+	}
+}
+
+void DisplayServerEmbedded::send_window_event(DisplayServer::WindowEvent p_event, WindowID p_id) const {
+	const Callable *cb = window_event_callbacks.getptr(p_id);
+	if (cb) {
+		_window_callback(*cb, int(p_event));
+	}
+}
+
+void DisplayServerEmbedded::_window_callback(const Callable &p_callable, const Variant &p_arg) const {
+	if (p_callable.is_valid()) {
+		p_callable.call(p_arg);
+	}
+}
+
+// MARK: -
+
+bool DisplayServerEmbedded::has_feature(Feature p_feature) const {
+	switch (p_feature) {
+#ifndef DISABLE_DEPRECATED
+		case FEATURE_GLOBAL_MENU: {
+			return (native_menu && native_menu->has_feature(NativeMenu::FEATURE_GLOBAL_MENU));
+		} break;
+#endif
+		case FEATURE_CURSOR_SHAPE:
+		case FEATURE_IME:
+			// case FEATURE_CUSTOM_CURSOR_SHAPE:
+			// case FEATURE_HIDPI:
+			// case FEATURE_ICON:
+			// case FEATURE_MOUSE:
+			// case FEATURE_MOUSE_WARP:
+			// case FEATURE_NATIVE_DIALOG:
+			// case FEATURE_NATIVE_ICON:
+			// case FEATURE_WINDOW_TRANSPARENCY:
+			// case FEATURE_CLIPBOARD:
+			// case FEATURE_KEEP_SCREEN_ON:
+			// case FEATURE_ORIENTATION:
+			// case FEATURE_VIRTUAL_KEYBOARD:
+			// case FEATURE_TEXT_TO_SPEECH:
+			// case FEATURE_TOUCHSCREEN:
+			return true;
+		default:
+			return false;
+	}
+}
+
+String DisplayServerEmbedded::get_name() const {
+	return "embedded";
+}
+
+int DisplayServerEmbedded::get_screen_count() const {
+	return 1;
+}
+
+int DisplayServerEmbedded::get_primary_screen() const {
+	return 0;
+}
+
+Point2i DisplayServerEmbedded::screen_get_position(int p_screen) const {
+	return Size2i();
+}
+
+Size2i DisplayServerEmbedded::screen_get_size(int p_screen) const {
+	return window_get_size(MAIN_WINDOW_ID);
+}
+
+Rect2i DisplayServerEmbedded::screen_get_usable_rect(int p_screen) const {
+	return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen));
+}
+
+int DisplayServerEmbedded::screen_get_dpi(int p_screen) const {
+	return 96;
+}
+
+float DisplayServerEmbedded::screen_get_refresh_rate(int p_screen) const {
+	_THREAD_SAFE_METHOD_
+
+	p_screen = _get_screen_index(p_screen);
+	NSArray *screenArray = [NSScreen screens];
+	if ((NSUInteger)p_screen < [screenArray count]) {
+		NSDictionary *description = [[screenArray objectAtIndex:p_screen] deviceDescription];
+		const CGDisplayModeRef displayMode = CGDisplayCopyDisplayMode([[description objectForKey:@"NSScreenNumber"] unsignedIntValue]);
+		const double displayRefreshRate = CGDisplayModeGetRefreshRate(displayMode);
+		return (float)displayRefreshRate;
+	}
+	ERR_PRINT("An error occurred while trying to get the screen refresh rate.");
+	return SCREEN_REFRESH_RATE_FALLBACK;
+}
+
+Vector<DisplayServer::WindowID> DisplayServerEmbedded::get_window_list() const {
+	Vector<DisplayServer::WindowID> list;
+	list.push_back(MAIN_WINDOW_ID);
+	return list;
+}
+
+DisplayServer::WindowID DisplayServerEmbedded::get_window_at_screen_position(const Point2i &p_position) const {
+	return MAIN_WINDOW_ID;
+}
+
+void DisplayServerEmbedded::window_attach_instance_id(ObjectID p_instance, WindowID p_window) {
+	window_attached_instance_id[p_window] = p_instance;
+}
+
+ObjectID DisplayServerEmbedded::window_get_attached_instance_id(WindowID p_window) const {
+	return window_attached_instance_id[p_window];
+}
+
+void DisplayServerEmbedded::window_set_title(const String &p_title, WindowID p_window) {
+	// Not supported
+}
+
+int DisplayServerEmbedded::window_get_current_screen(WindowID p_window) const {
+	return SCREEN_OF_MAIN_WINDOW;
+}
+
+void DisplayServerEmbedded::window_set_current_screen(int p_screen, WindowID p_window) {
+	// Not supported
+}
+
+Point2i DisplayServerEmbedded::window_get_position(WindowID p_window) const {
+	return Point2i();
+}
+
+Point2i DisplayServerEmbedded::window_get_position_with_decorations(WindowID p_window) const {
+	return Point2i();
+}
+
+void DisplayServerEmbedded::window_set_position(const Point2i &p_position, WindowID p_window) {
+	// Probably not supported for single window iOS app
+}
+
+void DisplayServerEmbedded::window_set_transient(WindowID p_window, WindowID p_parent) {
+	// Not supported
+}
+
+void DisplayServerEmbedded::window_set_max_size(const Size2i p_size, WindowID p_window) {
+	// Not supported
+}
+
+Size2i DisplayServerEmbedded::window_get_max_size(WindowID p_window) const {
+	return Size2i();
+}
+
+void DisplayServerEmbedded::window_set_min_size(const Size2i p_size, WindowID p_window) {
+	// Not supported
+}
+
+Size2i DisplayServerEmbedded::window_get_min_size(WindowID p_window) const {
+	return Size2i();
+}
+
+void DisplayServerEmbedded::window_set_size(const Size2i p_size, WindowID p_window) {
+	[CATransaction begin];
+	[CATransaction setDisableActions:YES];
+
+	// TODO(sgc): Pass scale as argument from parent process.
+	constexpr CGFloat CONTENT_SCALE = 2.0;
+	CGRect bounds = CGRectMake(0, 0, p_size.width, p_size.height);
+	bounds = CGRectApplyAffineTransform(bounds, CGAffineTransformMakeScale(1.0 / CONTENT_SCALE, 1.0 / CONTENT_SCALE));
+	layer.bounds = bounds;
+
+#if defined(RD_ENABLED)
+	if (rendering_context) {
+		rendering_context->window_set_size(p_window, p_size.width, p_size.height);
+	}
+#endif
+#if defined(GLES3_ENABLED)
+	if (gl_manager) {
+		gl_manager->window_resize(p_window, p_size.width, p_size.height);
+	}
+#endif
+	[CATransaction commit];
+
+	Callable *cb = window_resize_callbacks.getptr(p_window);
+	if (cb) {
+		Variant resize_rect = Rect2i(Point2i(), p_size);
+		_window_callback(window_resize_callbacks[p_window], resize_rect);
+	}
+}
+
+Size2i DisplayServerEmbedded::window_get_size(WindowID p_window) const {
+#if defined(RD_ENABLED)
+	if (rendering_context) {
+		RenderingContextDriver::SurfaceID surface = rendering_context->surface_get_from_window(p_window);
+		ERR_FAIL_COND_V_MSG(surface == 0, Size2i(), "Invalid window ID");
+		uint32_t width = rendering_context->surface_get_width(surface);
+		uint32_t height = rendering_context->surface_get_height(surface);
+		return Size2i(width, height);
+	}
+#endif
+#ifdef GLES3_ENABLED
+	if (gl_manager) {
+		return gl_manager->window_get_size(p_window);
+	}
+#endif
+	return Size2i();
+}
+
+Size2i DisplayServerEmbedded::window_get_size_with_decorations(WindowID p_window) const {
+	return window_get_size(p_window);
+}
+
+void DisplayServerEmbedded::window_set_mode(WindowMode p_mode, WindowID p_window) {
+	// Not supported
+}
+
+DisplayServer::WindowMode DisplayServerEmbedded::window_get_mode(WindowID p_window) const {
+	return WindowMode::WINDOW_MODE_WINDOWED;
+}
+
+bool DisplayServerEmbedded::window_is_maximize_allowed(WindowID p_window) const {
+	return false;
+}
+
+void DisplayServerEmbedded::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) {
+	// Not supported
+}
+
+bool DisplayServerEmbedded::window_get_flag(WindowFlags p_flag, WindowID p_window) const {
+	return false;
+}
+
+void DisplayServerEmbedded::window_request_attention(WindowID p_window) {
+	// Not supported
+}
+
+void DisplayServerEmbedded::window_move_to_foreground(WindowID p_window) {
+	// Not supported
+}
+
+bool DisplayServerEmbedded::window_is_focused(WindowID p_window) const {
+	return true;
+}
+
+float DisplayServerEmbedded::screen_get_max_scale() const {
+	return state.screen_max_scale;
+}
+
+bool DisplayServerEmbedded::window_can_draw(WindowID p_window) const {
+	return true;
+}
+
+bool DisplayServerEmbedded::can_any_window_draw() const {
+	return true;
+}
+
+void DisplayServerEmbedded::window_set_ime_active(const bool p_active, WindowID p_window) {
+	EngineDebugger::get_singleton()->send_message("game_view:window_set_ime_active", { p_active });
+}
+
+void DisplayServerEmbedded::window_set_ime_position(const Point2i &p_pos, WindowID p_window) {
+	if (p_pos == ime_last_position) {
+		return;
+	}
+	EngineDebugger::get_singleton()->send_message("game_view:window_set_ime_position", { p_pos });
+	ime_last_position = p_pos;
+}
+
+void DisplayServerEmbedded::update_state(const Dictionary &p_state) {
+	state.screen_max_scale = p_state["screen_get_max_scale"];
+}
+
+void DisplayServerEmbedded::set_content_scale(float p_scale) {
+	content_scale = p_scale;
+}
+
+void DisplayServerEmbedded::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window) {
+	// Not supported
+}
+
+DisplayServer::VSyncMode DisplayServerEmbedded::window_get_vsync_mode(WindowID p_window) const {
+	_THREAD_SAFE_METHOD_
+#if defined(RD_ENABLED)
+	if (rendering_context) {
+		return rendering_context->window_get_vsync_mode(p_window);
+	}
+#endif
+	return DisplayServer::VSYNC_ENABLED;
+}
+
+void DisplayServerEmbedded::update_im_text(const Point2i &p_selection, const String &p_text) {
+	im_selection = p_selection;
+	im_text = p_text;
+
+	OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_IME_UPDATE);
+}
+
+Point2i DisplayServerEmbedded::ime_get_selection() const {
+	return im_selection;
+}
+
+String DisplayServerEmbedded::ime_get_text() const {
+	return im_text;
+}
+
+void DisplayServerEmbedded::cursor_set_shape(CursorShape p_shape) {
+	cursor_shape = p_shape;
+	EngineDebugger::get_singleton()->send_message("game_view:cursor_set_shape", { p_shape });
+}
+
+DisplayServer::CursorShape DisplayServerEmbedded::cursor_get_shape() const {
+	return cursor_shape;
+}
+
+void DisplayServerEmbedded::cursor_set_custom_image(const Ref<Resource> &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) {
+	WARN_PRINT_ONCE("Custom cursor images are not supported in embedded mode.");
+}
+
+void DisplayServerEmbedded::swap_buffers() {
+#ifdef GLES3_ENABLED
+	if (gl_manager) {
+		gl_manager->swap_buffers();
+	}
+#endif
+}

+ 28 - 4
platform/macos/display_server_macos.h

@@ -51,6 +51,7 @@
 #endif
 #endif // RD_ENABLED
 
+#define FontVariation __FontVariation
 #define BitMap _QDBitMap // Suppress deprecated QuickDraw definition.
 
 #import <AppKit/AppKit.h>
@@ -60,8 +61,18 @@
 #import <Foundation/Foundation.h>
 #import <IOKit/pwr_mgt/IOPMLib.h>
 
+@class GodotWindow;
+@class GodotContentView;
+@class GodotWindowDelegate;
+@class GodotButtonView;
+@class GodotEmbeddedView;
+@class CALayerHost;
+
 #undef BitMap
 #undef CursorShape
+#undef FontVariation
+
+class EmbeddedProcessMacOS;
 
 class DisplayServerMacOS : public DisplayServer {
 	GDSOFTCLASS(DisplayServerMacOS, DisplayServer);
@@ -83,10 +94,10 @@ public:
 	};
 
 	struct WindowData {
-		id window_delegate;
-		id window_object;
-		id window_view;
-		id window_button_view;
+		GodotWindowDelegate *window_delegate;
+		GodotWindow *window_object;
+		GodotContentView *window_view;
+		GodotButtonView *window_button_view;
 
 		Vector<Vector2> mpath;
 
@@ -241,6 +252,12 @@ private:
 
 	void initialize_tts() const;
 
+	struct EmbeddedProcessData {
+		const EmbeddedProcessMacOS *process;
+		CALayer *layer_host = nil;
+	};
+	HashMap<OS::ProcessID, EmbeddedProcessData> embedded_processes;
+
 public:
 	void menu_callback(id p_sender);
 
@@ -442,6 +459,13 @@ public:
 
 	virtual bool get_swap_cancel_ok() override;
 
+	virtual void enable_for_stealing_focus(OS::ProcessID pid) override;
+#ifdef DEBUG_ENABLED
+	Error embed_process_update(WindowID p_window, const EmbeddedProcessMacOS *p_process);
+#endif
+	virtual Error request_close_embedded_process(OS::ProcessID p_pid) override;
+	virtual Error remove_embedded_process(OS::ProcessID p_pid) override;
+
 	virtual int keyboard_get_layout_count() const override;
 	virtual int keyboard_get_current_layout() const override;
 	virtual void keyboard_set_current_layout(int p_index) override;

+ 88 - 20
platform/macos/display_server_macos.mm

@@ -30,6 +30,10 @@
 
 #import "display_server_macos.h"
 
+#ifdef DEBUG_ENABLED
+#import "editor/embedded_process_macos.h"
+#endif
+#import "godot_application.h"
 #import "godot_application_delegate.h"
 #import "godot_button_view.h"
 #import "godot_content_view.h"
@@ -40,6 +44,7 @@
 #import "godot_window.h"
 #import "godot_window_delegate.h"
 #import "key_mapping_macos.h"
+#import "macos_quartz_core_spi.h"
 #import "os_macos.h"
 #import "tts_macos.h"
 
@@ -140,7 +145,7 @@ DisplayServerMacOS::WindowID DisplayServerMacOS::_create_window(WindowMode p_mod
 			[wd.window_object setTabbingMode:NSWindowTabbingModeDisallowed];
 		}
 
-		CALayer *layer = [(NSView *)wd.window_view layer];
+		CALayer *layer = [wd.window_view layer];
 		if (layer) {
 			layer.contentsScale = scale;
 		}
@@ -198,7 +203,6 @@ DisplayServerMacOS::WindowID DisplayServerMacOS::_create_window(WindowMode p_mod
 			}
 		}
 		if (gl_manager_angle) {
-			CALayer *layer = [(NSView *)wd.window_view layer];
 			Error err = gl_manager_angle->window_create(window_id_counter, nullptr, (__bridge void *)layer, p_rect.size.width, p_rect.size.height);
 			if (err != OK) {
 				gl_failed = true;
@@ -880,6 +884,7 @@ bool DisplayServerMacOS::has_feature(Feature p_feature) const {
 		case FEATURE_WINDOW_DRAG:
 		case FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE:
 		case FEATURE_EMOJI_AND_SYMBOL_PICKER:
+		case FEATURE_WINDOW_EMBEDDING:
 			return true;
 #ifdef ACCESSKIT_ENABLED
 		case FEATURE_ACCESSIBILITY_SCREEN_READER: {
@@ -1765,11 +1770,10 @@ float DisplayServerMacOS::screen_get_scale(int p_screen) const {
 
 	p_screen = _get_screen_index(p_screen);
 	if (OS::get_singleton()->is_hidpi_allowed()) {
-		NSArray *screenArray = [NSScreen screens];
-		if ((NSUInteger)p_screen < [screenArray count]) {
-			if ([[screenArray objectAtIndex:p_screen] respondsToSelector:@selector(backingScaleFactor)]) {
-				return std::fmax(1.0, [[screenArray objectAtIndex:p_screen] backingScaleFactor]);
-			}
+		NSArray<NSScreen *> *screens = NSScreen.screens;
+		NSUInteger index = (NSUInteger)p_screen;
+		if (index < screens.count) {
+			return std::fmax(1.0f, screens[index].backingScaleFactor);
 		}
 	}
 
@@ -2010,8 +2014,8 @@ void DisplayServerMacOS::show_window(WindowID p_id) {
 	WindowData &wd = windows[p_id];
 
 	if (p_id == MAIN_WINDOW_ID) {
-		[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
-		static_cast<OS_MacOS *>(OS::get_singleton())->activate();
+		[GodotApp setActivationPolicy:NSApplicationActivationPolicyRegular];
+		[GodotApp activateApplication];
 	}
 
 	popup_open(p_id);
@@ -2033,6 +2037,7 @@ void DisplayServerMacOS::delete_sub_window(WindowID p_id) {
 	WindowData &wd = windows[p_id];
 
 	[wd.window_object setContentView:nil];
+	// This will cause the delegate to release the window.
 	[wd.window_object close];
 
 	mouse_enter_window(get_window_at_screen_position(mouse_get_position()));
@@ -2601,19 +2606,13 @@ bool DisplayServerMacOS::window_is_maximize_allowed(WindowID p_window) const {
 }
 
 bool DisplayServerMacOS::window_maximize_on_title_dbl_click() const {
-	id value = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleActionOnDoubleClick"];
-	if ([value isKindOfClass:[NSString class]]) {
-		return [value isEqualToString:@"Maximize"];
-	}
-	return false;
+	NSString *value = [NSUserDefaults.standardUserDefaults stringForKey:@"AppleActionOnDoubleClick"];
+	return [value isEqualToString:@"Maximize"];
 }
 
 bool DisplayServerMacOS::window_minimize_on_title_dbl_click() const {
-	id value = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleActionOnDoubleClick"];
-	if ([value isKindOfClass:[NSString class]]) {
-		return [value isEqualToString:@"Minimize"];
-	}
-	return false;
+	NSString *value = [NSUserDefaults.standardUserDefaults stringForKey:@"AppleActionOnDoubleClick"];
+	return [value isEqualToString:@"Minimize"];
 }
 
 void DisplayServerMacOS::window_start_drag(WindowID p_window) {
@@ -2645,7 +2644,7 @@ void DisplayServerMacOS::window_set_window_buttons_offset(const Vector2i &p_offs
 	wd.wb_offset = p_offset / scale;
 	wd.wb_offset = wd.wb_offset.maxi(12);
 	if (wd.window_button_view) {
-		[(GodotButtonView *)wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)];
+		[wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)];
 	}
 }
 
@@ -3275,6 +3274,75 @@ bool DisplayServerMacOS::get_swap_cancel_ok() {
 	return false;
 }
 
+void DisplayServerMacOS::enable_for_stealing_focus(OS::ProcessID pid) {
+}
+
+#define GET_OR_FAIL_V(m_val, m_map, m_key, m_retval) \
+	m_val = m_map.getptr(m_key);                     \
+	if (m_val == nullptr) {                          \
+		ERR_FAIL_V(m_retval);                        \
+	}
+
+#ifdef DEBUG_ENABLED
+
+Error DisplayServerMacOS::embed_process_update(WindowID p_window, const EmbeddedProcessMacOS *p_process) {
+	_THREAD_SAFE_METHOD_
+
+	WindowData *wd;
+	GET_OR_FAIL_V(wd, windows, p_window, FAILED);
+
+	OS::ProcessID p_pid = p_process->get_embedded_pid();
+
+	[CATransaction begin];
+	[CATransaction setDisableActions:YES];
+
+	EmbeddedProcessData *ed = embedded_processes.getptr(p_pid);
+	if (ed == nil) {
+		ed = &embedded_processes.insert(p_pid, EmbeddedProcessData())->value;
+
+		ed->process = p_process;
+
+		CALayerHost *host = [CALayerHost new];
+		uint32_t p_context_id = p_process->get_context_id();
+		host.contextId = static_cast<CAContextID>(p_context_id);
+		host.contentsScale = wd->window_object.backingScaleFactor;
+		host.contentsGravity = kCAGravityCenter;
+		ed->layer_host = host;
+		[wd->window_view.layer addSublayer:host];
+	}
+
+	Rect2i p_rect = p_process->get_screen_embedded_window_rect();
+	CGRect rect = CGRectMake(p_rect.position.x, p_rect.position.y, p_rect.size.x, p_rect.size.y);
+	rect = CGRectApplyAffineTransform(rect, CGAffineTransformMakeScale(0.5, 0.5));
+
+	CGFloat height = wd->window_view.frame.size.height;
+	CGFloat x = rect.origin.x;
+	CGFloat y = (height - rect.origin.y);
+	ed->layer_host.position = CGPointMake(x, y);
+	ed->layer_host.hidden = !p_process->is_visible_in_tree();
+
+	[CATransaction commit];
+
+	return OK;
+}
+
+#endif
+
+Error DisplayServerMacOS::request_close_embedded_process(OS::ProcessID p_pid) {
+	return OK;
+}
+
+Error DisplayServerMacOS::remove_embedded_process(OS::ProcessID p_pid) {
+	_THREAD_SAFE_METHOD_
+
+	EmbeddedProcessData *ed;
+	GET_OR_FAIL_V(ed, embedded_processes, p_pid, ERR_DOES_NOT_EXIST);
+	[ed->layer_host removeFromSuperlayer];
+	embedded_processes.erase(p_pid);
+
+	return OK;
+}
+
 int DisplayServerMacOS::keyboard_get_layout_count() const {
 	if (keyboard_layout_dirty) {
 		_update_keyboard_layouts();

+ 75 - 0
platform/macos/editor/embedded_game_view_plugin.h

@@ -0,0 +1,75 @@
+/**************************************************************************/
+/*  embedded_game_view_plugin.h                                           */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "editor/plugins/game_view_plugin.h"
+
+class EmbeddedProcessMacOS;
+
+class GameViewDebuggerMacOS : public GameViewDebugger {
+	GDCLASS(GameViewDebuggerMacOS, GameViewDebugger);
+
+	EmbeddedProcessMacOS *embedded_process = nullptr;
+
+	/// Message handler function for capture.
+
+	/// @brief A function pointer to the message handler function.
+	typedef bool (GameViewDebuggerMacOS::*ParseMessageFunc)(const Array &p_args);
+
+	/// @brief A map of message handlers.
+	static HashMap<String, ParseMessageFunc> parse_message_handlers;
+
+	/// @brief Initialize the message handlers.
+	static void _init_capture_message_handlers();
+
+	bool _msg_set_context_id(const Array &p_args);
+	bool _msg_cursor_set_shape(const Array &p_args);
+	bool _msg_mouse_set_mode(const Array &p_args);
+	bool _msg_window_set_ime_active(const Array &p_args);
+	bool _msg_window_set_ime_position(const Array &p_args);
+	bool _msg_joy_start(const Array &p_args);
+	bool _msg_joy_stop(const Array &p_args);
+
+public:
+	virtual bool capture(const String &p_message, const Array &p_data, int p_session) override;
+	virtual bool has_capture(const String &p_capture) const override;
+
+	GameViewDebuggerMacOS(EmbeddedProcessMacOS *p_embedded_process);
+};
+
+class GameViewPluginMacOS : public GameViewPluginBase {
+	GDCLASS(GameViewPluginMacOS, GameViewPluginBase);
+
+public:
+	GameViewPluginMacOS();
+};
+
+extern "C" void register_game_view_plugin();

+ 154 - 0
platform/macos/editor/embedded_game_view_plugin.mm

@@ -0,0 +1,154 @@
+/**************************************************************************/
+/*  embedded_game_view_plugin.mm                                          */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "embedded_game_view_plugin.h"
+
+#include "embedded_process_macos.h"
+
+#include "editor/editor_node.h"
+#include "editor/window_wrapper.h"
+
+HashMap<String, GameViewDebuggerMacOS::ParseMessageFunc> GameViewDebuggerMacOS::parse_message_handlers;
+
+bool GameViewDebuggerMacOS::_msg_set_context_id(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 1, false, "set_context_id: invalid number of arguments");
+
+	embedded_process->set_context_id(p_args[0]);
+	return true;
+}
+
+bool GameViewDebuggerMacOS::_msg_cursor_set_shape(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 1, false, "cursor_set_shape: invalid number of arguments");
+
+	Control::CursorShape shape = Control::CursorShape(p_args[0]);
+	embedded_process->get_layer_host()->set_default_cursor_shape(static_cast<Control::CursorShape>(shape));
+
+	return true;
+}
+
+bool GameViewDebuggerMacOS::_msg_mouse_set_mode(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 1, false, "mouse_set_mode: invalid number of arguments");
+
+	DisplayServer::MouseMode mode = DisplayServer::MouseMode(p_args[0]);
+	embedded_process->mouse_set_mode(mode);
+
+	return true;
+}
+
+bool GameViewDebuggerMacOS::_msg_window_set_ime_active(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 1, false, "window_set_ime_active: invalid number of arguments");
+
+	bool active = p_args[0];
+	DisplayServer::WindowID wid = embedded_process->get_window()->get_window_id();
+	DisplayServer::get_singleton()->window_set_ime_active(active, wid);
+	return true;
+}
+
+bool GameViewDebuggerMacOS::_msg_window_set_ime_position(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 1, false, "window_set_ime_position: invalid number of arguments");
+
+	Point2i pos = p_args[0];
+	Point2i xpos = embedded_process->get_layer_host()->get_global_transform_with_canvas().xform(pos);
+	DisplayServer::WindowID wid = embedded_process->get_window()->get_window_id();
+	DisplayServer::get_singleton()->window_set_ime_position(xpos, wid);
+	return true;
+}
+
+bool GameViewDebuggerMacOS::_msg_joy_start(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 3, false, "joy_start: invalid number of arguments");
+
+	int joy_id = p_args[0];
+	float duration = p_args[1];
+	Vector2 strength = p_args[2];
+	Input::get_singleton()->start_joy_vibration(joy_id, strength.x, strength.y, duration);
+	return true;
+}
+
+bool GameViewDebuggerMacOS::_msg_joy_stop(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 1, false, "joy_stop: invalid number of arguments");
+
+	int joy_id = p_args[0];
+	Input::get_singleton()->stop_joy_vibration(joy_id);
+	return true;
+}
+
+void GameViewDebuggerMacOS::_init_capture_message_handlers() {
+	parse_message_handlers["game_view:set_context_id"] = &GameViewDebuggerMacOS::_msg_set_context_id;
+	parse_message_handlers["game_view:cursor_set_shape"] = &GameViewDebuggerMacOS::_msg_cursor_set_shape;
+	parse_message_handlers["game_view:mouse_set_mode"] = &GameViewDebuggerMacOS::_msg_mouse_set_mode;
+	parse_message_handlers["game_view:window_set_ime_active"] = &GameViewDebuggerMacOS::_msg_window_set_ime_active;
+	parse_message_handlers["game_view:window_set_ime_position"] = &GameViewDebuggerMacOS::_msg_window_set_ime_position;
+	parse_message_handlers["game_view:joy_start"] = &GameViewDebuggerMacOS::_msg_joy_start;
+	parse_message_handlers["game_view:joy_stop"] = &GameViewDebuggerMacOS::_msg_joy_stop;
+}
+
+bool GameViewDebuggerMacOS::has_capture(const String &p_capture) const {
+	return p_capture == "game_view";
+}
+
+bool GameViewDebuggerMacOS::capture(const String &p_message, const Array &p_data, int p_session) {
+	Ref<EditorDebuggerSession> session = get_session(p_session);
+	ERR_FAIL_COND_V(session.is_null(), true);
+
+	ParseMessageFunc *fn_ptr = parse_message_handlers.getptr(p_message);
+	if (fn_ptr) {
+		return (this->**fn_ptr)(p_data);
+	} else {
+		// Any other messages with this prefix should be ignored.
+		WARN_PRINT("GameViewDebuggerMacOS unknown message: " + p_message);
+		return ERR_SKIP;
+	}
+
+	return true;
+}
+
+GameViewDebuggerMacOS::GameViewDebuggerMacOS(EmbeddedProcessMacOS *p_embedded_process) :
+		embedded_process(p_embedded_process) {
+	if (parse_message_handlers.is_empty()) {
+		_init_capture_message_handlers();
+	}
+}
+
+GameViewPluginMacOS::GameViewPluginMacOS() {
+	if (Engine::get_singleton()->is_recovery_mode_hint()) {
+		return;
+	}
+
+	EmbeddedProcessMacOS *embedded_process = memnew(EmbeddedProcessMacOS);
+
+	Ref<GameViewDebuggerMacOS> debugger;
+	debugger.instantiate(embedded_process);
+
+	setup(debugger, embedded_process);
+}
+
+extern "C" GameViewPluginBase *get_game_view_plugin() {
+	return memnew(GameViewPluginMacOS);
+}

+ 110 - 0
platform/macos/editor/embedded_process_macos.h

@@ -0,0 +1,110 @@
+/**************************************************************************/
+/*  embedded_process_macos.h                                              */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "editor/plugins/embedded_process.h"
+
+class DisplayServerMacOS;
+
+class LayerHost : public Control {
+	GDCLASS(LayerHost, Control);
+
+	ScriptEditorDebugger *script_debugger = nullptr;
+
+	virtual void gui_input(const Ref<InputEvent> &p_event) override;
+
+protected:
+	void _notification(int p_what);
+
+public:
+	void set_script_debugger(ScriptEditorDebugger *p_debugger) {
+		script_debugger = p_debugger;
+	}
+};
+
+class EmbeddedProcessMacOS final : public EmbeddedProcessBase {
+	GDCLASS(EmbeddedProcessMacOS, EmbeddedProcessBase);
+
+	enum class EmbeddingState {
+		IDLE,
+		IN_PROGRESS,
+		COMPLETED,
+		FAILED,
+	};
+
+	DisplayServerMacOS *ds = nullptr;
+	EmbeddingState embedding_state = EmbeddingState::IDLE;
+	uint32_t context_id = 0;
+	ScriptEditorDebugger *script_debugger = nullptr;
+	LayerHost *layer_host = nullptr;
+	OS::ProcessID current_process_id = 0;
+
+	// Embedded process state.
+
+	/// @brief The current mouse mode of the embedded process.
+	DisplayServer::MouseMode mouse_mode = DisplayServer::MOUSE_MODE_VISIBLE;
+
+	void _try_embed_process();
+	void update_embedded_process() const;
+	void _joy_connection_changed(int p_index, bool p_connected) const;
+
+protected:
+	void _notification(int p_what);
+
+public:
+	// MARK: - Message Handlers
+	void set_context_id(uint32_t p_context_id);
+
+	uint32_t get_context_id() const { return context_id; }
+	void set_script_debugger(ScriptEditorDebugger *p_debugger) override;
+
+	bool is_embedding_in_progress() const override {
+		return embedding_state == EmbeddingState::IN_PROGRESS;
+	}
+
+	bool is_embedding_completed() const override {
+		return embedding_state == EmbeddingState::COMPLETED;
+	}
+
+	virtual bool is_process_focused() const override { return layer_host->has_focus(); }
+	virtual void embed_process(OS::ProcessID p_pid) override;
+	virtual int get_embedded_pid() const override { return current_process_id; }
+	virtual void reset() override;
+	virtual void request_close() override;
+	virtual void queue_update_embedded_process() override { update_embedded_process(); }
+
+	Rect2i get_adjusted_embedded_window_rect(const Rect2i &p_rect) const override;
+
+	void mouse_set_mode(DisplayServer::MouseMode p_mode);
+	_FORCE_INLINE_ LayerHost *get_layer_host() const { return layer_host; }
+
+	EmbeddedProcessMacOS();
+};

+ 248 - 0
platform/macos/editor/embedded_process_macos.mm

@@ -0,0 +1,248 @@
+/**************************************************************************/
+/*  embedded_process_macos.mm                                             */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "embedded_process_macos.h"
+
+#include "platform/macos/display_server_macos.h"
+
+#include "core/input/input_event_codec.h"
+#include "editor/debugger/script_editor_debugger.h"
+#include "editor/editor_settings.h"
+#include "scene/gui/control.h"
+#include "scene/main/window.h"
+
+void EmbeddedProcessMacOS::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_RESIZED:
+		case NOTIFICATION_VISIBILITY_CHANGED: {
+			update_embedded_process();
+		} break;
+	}
+}
+
+void EmbeddedProcessMacOS::update_embedded_process() const {
+	layer_host->set_rect(get_adjusted_embedded_window_rect(get_rect()));
+	if (is_embedding_completed()) {
+		ds->embed_process_update(window->get_window_id(), this);
+		Rect2i rect = get_screen_embedded_window_rect();
+		script_debugger->send_message("embed:window_size", { rect.size });
+	}
+}
+
+void EmbeddedProcessMacOS::set_context_id(uint32_t p_context_id) {
+	if (!window) {
+		return;
+	}
+
+	context_id = p_context_id;
+
+	_try_embed_process();
+}
+
+void EmbeddedProcessMacOS::set_script_debugger(ScriptEditorDebugger *p_debugger) {
+	script_debugger = p_debugger;
+	layer_host->set_script_debugger(script_debugger);
+	_try_embed_process();
+}
+
+void EmbeddedProcessMacOS::embed_process(OS::ProcessID p_pid) {
+	if (!window) {
+		return;
+	}
+
+	if (current_process_id != 0) {
+		// Stop embedding the last process.
+		OS::get_singleton()->kill(current_process_id);
+	}
+
+	reset();
+
+	current_process_id = p_pid;
+	embedding_state = EmbeddingState::IN_PROGRESS;
+	// Attempt to embed the process, but if it has just started and the window is not ready yet,
+	// we will retry in this case.
+	_try_embed_process();
+}
+
+void EmbeddedProcessMacOS::_joy_connection_changed(int p_index, bool p_connected) const {
+	if (!script_debugger) {
+		return;
+	}
+
+	if (p_connected) {
+		String name = Input::get_singleton()->get_joy_name(p_index);
+		script_debugger->send_message("embed:joy_add", { p_index, name });
+	} else {
+		script_debugger->send_message("embed:joy_del", { p_index });
+	}
+}
+
+void EmbeddedProcessMacOS::reset() {
+	if (!ds) {
+		ds = static_cast<DisplayServerMacOS *>(DisplayServer::get_singleton());
+	}
+	if (current_process_id != 0 && is_embedding_completed()) {
+		ds->remove_embedded_process(current_process_id);
+	}
+	current_process_id = 0;
+	embedding_state = EmbeddingState::IDLE;
+	context_id = 0;
+	script_debugger = nullptr;
+	queue_redraw();
+}
+
+void EmbeddedProcessMacOS::request_close() {
+	if (current_process_id != 0 && is_embedding_completed()) {
+		ds->request_close_embedded_process(current_process_id);
+	}
+}
+
+void EmbeddedProcessMacOS::_try_embed_process() {
+	if (current_process_id == 0 || script_debugger == nullptr || context_id == 0) {
+		return;
+	}
+
+	Error err = ds->embed_process_update(window->get_window_id(), this);
+	if (err == OK) {
+		Rect2i rect = get_screen_embedded_window_rect();
+		script_debugger->send_message("embed:window_size", { rect.size });
+		embedding_state = EmbeddingState::COMPLETED;
+		queue_redraw();
+		emit_signal(SNAME("embedding_completed"));
+
+		// Replicate some of the DisplayServer state.
+		{
+			Dictionary state;
+			state["screen_get_max_scale"] = ds->screen_get_max_scale();
+			// script_debugger->send_message("embed:ds_state", { state });
+		}
+
+		// Send initial joystick state.
+		{
+			Input *input = Input::get_singleton();
+			TypedArray<int> joy_pads = input->get_connected_joypads();
+			for (const Variant &idx : joy_pads) {
+				String name = input->get_joy_name(idx);
+				script_debugger->send_message("embed:joy_add", { idx, name });
+			}
+		}
+
+		layer_host->grab_focus();
+	} else {
+		// Another unknown error.
+		reset();
+		emit_signal(SNAME("embedding_failed"));
+	}
+}
+
+Rect2i EmbeddedProcessMacOS::get_adjusted_embedded_window_rect(const Rect2i &p_rect) const {
+	Rect2i control_rect = Rect2i(p_rect.position + margin_top_left, (p_rect.size - get_margins_size()).maxi(1));
+	if (window_size != Size2i()) {
+		Rect2i desired_rect;
+		if (!keep_aspect && control_rect.size.x >= window_size.x && control_rect.size.y >= window_size.y) {
+			// Fixed at the desired size.
+			desired_rect.size = window_size;
+		} else {
+			float ratio = MIN((float)control_rect.size.x / window_size.x, (float)control_rect.size.y / window_size.y);
+			desired_rect.size = Size2i(window_size.x * ratio, window_size.y * ratio).maxi(1);
+		}
+		desired_rect.position = Size2i(control_rect.position.x + ((control_rect.size.x - desired_rect.size.x) / 2), control_rect.position.y + ((control_rect.size.y - desired_rect.size.y) / 2));
+		return desired_rect;
+	} else {
+		// Stretch, use all the control area.
+		return control_rect;
+	}
+}
+
+void EmbeddedProcessMacOS::mouse_set_mode(DisplayServer::MouseMode p_mode) {
+	DisplayServer::get_singleton()->mouse_set_mode(p_mode);
+}
+
+EmbeddedProcessMacOS::EmbeddedProcessMacOS() :
+		EmbeddedProcessBase() {
+	layer_host = memnew(LayerHost);
+	add_child(layer_host);
+	layer_host->set_focus_mode(FOCUS_ALL);
+	layer_host->set_anchors_and_offsets_preset(PRESET_FULL_RECT);
+	layer_host->set_custom_minimum_size(Size2(100, 100));
+
+	Input *input = Input::get_singleton();
+	input->connect(SNAME("joy_connection_changed"), callable_mp(this, &EmbeddedProcessMacOS::_joy_connection_changed));
+
+	// This shortcut allows a user to forcibly release a captured mouse from within the editor, regardless of whether
+	// the embedded process has implemented support to release the cursor.
+	ED_SHORTCUT("game_view/release_mouse", TTRC("Release Mouse"), KeyModifierMask::ALT | Key::ESCAPE);
+}
+
+void LayerHost::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_FOCUS_ENTER: {
+			if (script_debugger) {
+				script_debugger->send_message("embed:win_event", { DisplayServer::WINDOW_EVENT_MOUSE_ENTER });
+			}
+		} break;
+		case NOTIFICATION_FOCUS_EXIT: {
+			if (script_debugger) {
+				script_debugger->send_message("embed:win_event", { DisplayServer::WINDOW_EVENT_MOUSE_EXIT });
+			}
+		} break;
+		case MainLoop::NOTIFICATION_OS_IME_UPDATE: {
+			if (script_debugger && has_focus()) {
+				const String ime_text = DisplayServer::get_singleton()->ime_get_text();
+				const Vector2i ime_selection = DisplayServer::get_singleton()->ime_get_selection();
+				script_debugger->send_message("embed:ime_update", { ime_text, ime_selection });
+			}
+		} break;
+	}
+}
+
+void LayerHost::gui_input(const Ref<InputEvent> &p_event) {
+	if (!script_debugger) {
+		return;
+	}
+
+	if (p_event->is_pressed()) {
+		if (ED_IS_SHORTCUT("game_view/release_mouse", p_event)) {
+			DisplayServer *ds = DisplayServer::get_singleton();
+			if (ds->mouse_get_mode() != DisplayServer::MOUSE_MODE_VISIBLE) {
+				ds->mouse_set_mode(DisplayServer::MOUSE_MODE_VISIBLE);
+				script_debugger->send_message("embed:mouse_set_mode", { DisplayServer::MOUSE_MODE_VISIBLE });
+			}
+			accept_event();
+			return;
+		}
+	}
+
+	PackedByteArray data;
+	if (encode_input_event(p_event, data)) {
+		script_debugger->send_message("embed:event", { data });
+		accept_event();
+	}
+}

+ 70 - 0
platform/macos/embedded_debugger.h

@@ -0,0 +1,70 @@
+/**************************************************************************/
+/*  embedded_debugger.h                                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "core/templates/hash_map.h"
+#include "core/variant/array.h"
+
+class DisplayServerEmbedded;
+
+/// @brief Singleton class to process embedded debugging message in the child process.
+class EmbeddedDebugger {
+	inline static EmbeddedDebugger *singleton = nullptr;
+
+	EmbeddedDebugger(DisplayServerEmbedded *p_ds);
+
+public:
+	static void initialize(DisplayServerEmbedded *p_ds);
+	static void deinitialize();
+
+	~EmbeddedDebugger();
+
+#ifdef DEBUG_ENABLED
+private:
+	DisplayServerEmbedded *ds;
+
+	/// Message handler function for parse_message.
+	typedef Error (EmbeddedDebugger::*ParseMessageFunc)(const Array &p_args);
+	static HashMap<String, ParseMessageFunc> parse_message_handlers;
+	static void _init_parse_message_handlers();
+
+	Error _msg_window_size(const Array &p_args);
+	Error _msg_mouse_set_mode(const Array &p_args);
+	Error _msg_event(const Array &p_args);
+	Error _msg_win_event(const Array &p_args);
+	Error _msg_ime_update(const Array &p_args);
+	Error _msg_joy_add(const Array &p_args);
+	Error _msg_joy_del(const Array &p_args);
+
+public:
+	static Error parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
+#endif
+};

+ 177 - 0
platform/macos/embedded_debugger.mm

@@ -0,0 +1,177 @@
+/**************************************************************************/
+/*  embedded_debugger.mm                                                  */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "embedded_debugger.h"
+
+#include "display_server_embedded.h"
+
+#include "core/debugger/engine_debugger.h"
+#include "core/input/input_event_codec.h"
+
+#ifdef DEBUG_ENABLED
+HashMap<String, EmbeddedDebugger::ParseMessageFunc> EmbeddedDebugger::parse_message_handlers;
+#endif
+
+EmbeddedDebugger::EmbeddedDebugger(DisplayServerEmbedded *p_ds) {
+	singleton = this;
+
+#ifdef DEBUG_ENABLED
+	ds = p_ds;
+	if (parse_message_handlers.is_empty()) {
+		_init_parse_message_handlers();
+	}
+	EngineDebugger::register_message_capture("embed", EngineDebugger::Capture(this, EmbeddedDebugger::parse_message));
+#endif
+}
+
+EmbeddedDebugger::~EmbeddedDebugger() {
+	singleton = nullptr;
+}
+
+void EmbeddedDebugger::initialize(DisplayServerEmbedded *p_ds) {
+	if (EngineDebugger::is_active()) {
+		memnew(EmbeddedDebugger(p_ds));
+	}
+}
+
+void EmbeddedDebugger::deinitialize() {
+	if (singleton) {
+		memdelete(singleton);
+	}
+}
+
+#ifdef DEBUG_ENABLED
+void EmbeddedDebugger::_init_parse_message_handlers() {
+	parse_message_handlers["window_size"] = &EmbeddedDebugger::_msg_window_size;
+	parse_message_handlers["mouse_set_mode"] = &EmbeddedDebugger::_msg_mouse_set_mode;
+	parse_message_handlers["event"] = &EmbeddedDebugger::_msg_event;
+	parse_message_handlers["win_event"] = &EmbeddedDebugger::_msg_win_event;
+	parse_message_handlers["ime_update"] = &EmbeddedDebugger::_msg_ime_update;
+	parse_message_handlers["joy_add"] = &EmbeddedDebugger::_msg_joy_add;
+	parse_message_handlers["joy_del"] = &EmbeddedDebugger::_msg_joy_del;
+}
+
+Error EmbeddedDebugger::_msg_window_size(const Array &p_args) {
+	Size2i size = p_args[0];
+	ds->window_set_size(size);
+	return OK;
+}
+
+Error EmbeddedDebugger::_msg_mouse_set_mode(const Array &p_args) {
+	DisplayServer::MouseMode mode = p_args[0];
+	ds->mouse_set_mode(mode);
+	return OK;
+}
+
+Error EmbeddedDebugger::_msg_event(const Array &p_args) {
+	Input *input = Input::get_singleton();
+	if (!input) {
+		// Ignore if we've received an event before the process has initialized.
+		return OK;
+	}
+
+	PackedByteArray data = p_args[0];
+	Ref<InputEvent> event;
+	decode_input_event(data, event);
+
+	{
+		Ref<InputEventMouse> e = event;
+		if (e.is_valid()) {
+			input->set_mouse_position(e->get_position());
+		}
+	}
+
+	{
+		Ref<InputEventMagnifyGesture> e = event;
+		if (e.is_valid()) {
+			input->set_mouse_position(e->get_position());
+		}
+	}
+
+	{
+		Ref<InputEventPanGesture> e = event;
+		if (e.is_valid()) {
+			input->set_mouse_position(e->get_position());
+		}
+	}
+
+	if (event.is_valid()) {
+		input->parse_input_event(event);
+	}
+
+	return OK;
+}
+
+Error EmbeddedDebugger::_msg_win_event(const Array &p_args) {
+	DisplayServer::WindowEvent win_event = p_args[0];
+	ds->send_window_event(win_event, DisplayServer::MAIN_WINDOW_ID);
+	if (win_event == DisplayServer::WindowEvent::WINDOW_EVENT_MOUSE_EXIT) {
+		Input::get_singleton()->release_pressed_events();
+	}
+	return OK;
+}
+
+Error EmbeddedDebugger::_msg_ime_update(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 2, ERR_INVALID_PARAMETER, "Invalid number of arguments for 'ime_update' message.");
+	String ime_text = p_args[0];
+	Vector2i ime_selection = p_args[1];
+	ds->update_im_text(ime_selection, ime_text);
+	return OK;
+}
+
+Error EmbeddedDebugger::_msg_joy_add(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 2, ERR_INVALID_PARAMETER, "Invalid number of arguments for 'joy_add' message.");
+	int idx = p_args[0];
+	String name = p_args[1];
+	ds->joy_add(idx, name);
+	return OK;
+}
+
+Error EmbeddedDebugger::_msg_joy_del(const Array &p_args) {
+	ERR_FAIL_COND_V_MSG(p_args.size() != 1, ERR_INVALID_PARAMETER, "Invalid number of arguments for 'joy_del' message.");
+	int idx = p_args[0];
+	ds->joy_del(idx);
+	return OK;
+}
+
+Error EmbeddedDebugger::parse_message(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured) {
+	EmbeddedDebugger *self = static_cast<EmbeddedDebugger *>(p_user);
+	r_captured = true;
+
+	ParseMessageFunc *fn_ptr = parse_message_handlers.getptr(p_msg);
+	if (fn_ptr) {
+		return (self->**fn_ptr)(p_args);
+	} else {
+		// Any other messages with this prefix should be ignored.
+		WARN_PRINT("Unknown message: " + p_msg);
+		return ERR_SKIP;
+	}
+}
+#endif

+ 108 - 0
platform/macos/embedded_gl_manager.h

@@ -0,0 +1,108 @@
+/**************************************************************************/
+/*  embedded_gl_manager.h                                                 */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#if defined(MACOS_ENABLED) && defined(GLES3_ENABLED)
+
+#include "core/os/os.h"
+#include "core/templates/local_vector.h"
+#include "servers/display_server.h"
+
+#import <AppKit/AppKit.h>
+#import <ApplicationServices/ApplicationServices.h>
+#import <CoreVideo/CoreVideo.h>
+
+GODOT_CLANG_WARNING_PUSH_AND_IGNORE("-Wdeprecated-declarations") // OpenGL is deprecated in macOS 10.14.
+
+typedef CGLContextObj (*CGLGetCurrentContextPtr)(void);
+typedef CGLError (*CGLTexImageIOSurface2DPtr)(CGLContextObj ctx, GLenum target, GLenum internal_format,
+		GLsizei width, GLsizei height, GLenum format, GLenum type, IOSurfaceRef ioSurface, GLuint plane);
+typedef const char *(*CGLErrorStringPtr)(CGLError);
+
+class GLManagerEmbedded {
+	/// @brief The number of framebuffers to create for each window.
+	///
+	/// Triple-buffering is used to avoid stuttering.
+	static constexpr uint32_t BUFFER_COUNT = 3;
+
+	struct FrameBuffer {
+		IOSurfaceRef surface = nullptr;
+		unsigned int tex = 0;
+		unsigned int fbo = 0;
+	};
+
+	struct GLWindow {
+		uint32_t width = 0;
+		uint32_t height = 0;
+		CALayer *layer = nullptr;
+		NSOpenGLContext *context = nullptr;
+		FrameBuffer framebuffers[BUFFER_COUNT];
+		uint32_t current_fb = 0;
+		bool is_valid = false;
+
+		void destroy_framebuffers();
+
+		~GLWindow() { destroy_framebuffers(); }
+	};
+
+	RBMap<DisplayServer::WindowID, GLWindow> windows;
+	typedef RBMap<DisplayServer::WindowID, GLWindow>::Element GLWindowElement;
+
+	NSOpenGLContext *shared_context = nullptr;
+	DisplayServer::WindowID current_window = DisplayServer::INVALID_WINDOW_ID;
+
+	Error create_context(GLWindow &p_win);
+
+	bool framework_loaded = false;
+	CGLGetCurrentContextPtr CGLGetCurrentContext = nullptr;
+	CGLTexImageIOSurface2DPtr CGLTexImageIOSurface2D = nullptr;
+	CGLErrorStringPtr CGLErrorString = nullptr;
+
+public:
+	Error window_create(DisplayServer::WindowID p_window_id, CALayer *p_layer, int p_width, int p_height);
+	void window_destroy(DisplayServer::WindowID p_window_id);
+	void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height);
+	Size2i window_get_size(DisplayServer::WindowID p_window_id) const;
+
+	void release_current();
+	void swap_buffers();
+
+	void window_make_current(DisplayServer::WindowID p_window_id);
+
+	Error initialize();
+
+	GLManagerEmbedded();
+	~GLManagerEmbedded();
+};
+
+GODOT_CLANG_WARNING_PUSH
+
+#endif // MACOS_ENABLED && GLES3_ENABLED

+ 271 - 0
platform/macos/embedded_gl_manager.mm

@@ -0,0 +1,271 @@
+/**************************************************************************/
+/*  embedded_gl_manager.mm                                                */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#import "embedded_gl_manager.h"
+
+#import "drivers/gles3/storage/texture_storage.h"
+#import "platform_gl.h"
+
+#if defined(MACOS_ENABLED) && defined(GLES3_ENABLED)
+
+#import <QuartzCore/QuartzCore.h>
+#include <dlfcn.h>
+
+GODOT_CLANG_WARNING_PUSH_AND_IGNORE("-Wdeprecated-declarations") // OpenGL is deprecated in macOS 10.14.
+
+Error GLManagerEmbedded::create_context(GLWindow &p_win) {
+	NSOpenGLPixelFormatAttribute attributes[] = {
+		NSOpenGLPFADoubleBuffer,
+		NSOpenGLPFAClosestPolicy,
+		NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
+		NSOpenGLPFAColorSize, 32,
+		NSOpenGLPFADepthSize, 24,
+		NSOpenGLPFAStencilSize, 8,
+		0
+	};
+
+	NSOpenGLPixelFormat *pixel_format = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
+	ERR_FAIL_NULL_V(pixel_format, ERR_CANT_CREATE);
+
+	p_win.context = [[NSOpenGLContext alloc] initWithFormat:pixel_format shareContext:shared_context];
+	ERR_FAIL_NULL_V(p_win.context, ERR_CANT_CREATE);
+	if (shared_context == nullptr) {
+		shared_context = p_win.context;
+	}
+
+	[p_win.context makeCurrentContext];
+
+	return OK;
+}
+
+Error GLManagerEmbedded::window_create(DisplayServer::WindowID p_window_id, CALayer *p_layer, int p_width, int p_height) {
+	GLWindow win;
+	win.layer = p_layer;
+	win.width = 0;
+	win.height = 0;
+
+	if (create_context(win) != OK) {
+		return FAILED;
+	}
+
+	windows[p_window_id] = win;
+	window_make_current(p_window_id);
+
+	return OK;
+}
+
+void GLManagerEmbedded::window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height) {
+	GLWindowElement *el = windows.find(p_window_id);
+	ERR_FAIL_NULL_MSG(el, "Window resize failed: window does not exist.");
+
+	GLWindow &win = el->get();
+
+	if (win.width == (uint32_t)p_width && win.height == (uint32_t)p_height) {
+		return;
+	}
+
+	win.width = (uint32_t)p_width;
+	win.height = (uint32_t)p_height;
+
+	win.destroy_framebuffers();
+
+	for (FrameBuffer &fb : win.framebuffers) {
+		NSDictionary *surfaceProps = @{
+			(NSString *)kIOSurfaceWidth : @(p_width),
+			(NSString *)kIOSurfaceHeight : @(p_height),
+			(NSString *)kIOSurfaceBytesPerElement : @(4),
+			(NSString *)kIOSurfacePixelFormat : @(kCVPixelFormatType_32BGRA),
+		};
+		fb.surface = IOSurfaceCreate((__bridge CFDictionaryRef)surfaceProps);
+		if (fb.surface == nullptr) {
+			ERR_PRINT(vformat("Failed to create IOSurface: width=%d, height=%d", p_width, p_height));
+			win.destroy_framebuffers();
+			return;
+		}
+
+		glGenTextures(1, &fb.tex);
+		glBindTexture(GL_TEXTURE_RECTANGLE, fb.tex);
+
+		glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+		glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+		glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+		glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+		CGLError err = CGLTexImageIOSurface2D(CGLGetCurrentContext(),
+				GL_TEXTURE_RECTANGLE,
+				GL_RGBA,
+				p_width,
+				p_height,
+				GL_BGRA,
+				GL_UNSIGNED_INT_8_8_8_8_REV,
+				fb.surface,
+				0);
+		if (err != kCGLNoError) {
+			String err_string = String(CGLErrorString(err));
+			ERR_PRINT(vformat("CGLTexImageIOSurface2D failed (%d): %s", err, err_string));
+			win.destroy_framebuffers();
+			return;
+		}
+
+		glGenFramebuffers(1, &fb.fbo);
+		glBindFramebuffer(GL_FRAMEBUFFER, fb.fbo);
+		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE, fb.tex, 0);
+
+		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
+			ERR_PRINT("Unable to create framebuffer from IOSurface texture.");
+			win.destroy_framebuffers();
+			return;
+		}
+
+		glBindFramebuffer(GL_FRAMEBUFFER, 0);
+		glBindTexture(GL_TEXTURE_RECTANGLE, 0);
+	}
+	win.current_fb = 0;
+	GLES3::TextureStorage::system_fbo = win.framebuffers[win.current_fb].fbo;
+	win.is_valid = true;
+}
+
+void GLManagerEmbedded::GLWindow::destroy_framebuffers() {
+	is_valid = false;
+	GLES3::TextureStorage::system_fbo = 0;
+
+	for (FrameBuffer &fb : framebuffers) {
+		if (fb.fbo) {
+			glDeleteFramebuffers(1, &fb.fbo);
+			fb.fbo = 0;
+		}
+
+		if (fb.tex) {
+			glDeleteTextures(1, &fb.tex);
+			fb.tex = 0;
+		}
+
+		if (fb.surface) {
+			IOSurfaceRef old_surface = fb.surface;
+			fb.surface = nullptr;
+			CFRelease(old_surface);
+		}
+	}
+}
+
+Size2i GLManagerEmbedded::window_get_size(DisplayServer::WindowID p_window_id) const {
+	const GLWindowElement *el = windows.find(p_window_id);
+	if (el == nullptr) {
+		return Size2i();
+	}
+
+	const GLWindow &win = el->value();
+	return Size2i(win.width, win.height);
+}
+
+void GLManagerEmbedded::window_destroy(DisplayServer::WindowID p_window_id) {
+	GLWindowElement *el = windows.find(p_window_id);
+	if (el == nullptr) {
+		return;
+	}
+
+	if (current_window == p_window_id) {
+		current_window = DisplayServer::INVALID_WINDOW_ID;
+	}
+
+	windows.erase(el);
+}
+
+void GLManagerEmbedded::release_current() {
+	if (current_window == DisplayServer::INVALID_WINDOW_ID) {
+		return;
+	}
+
+	[NSOpenGLContext clearCurrentContext];
+	current_window = DisplayServer::INVALID_WINDOW_ID;
+}
+
+void GLManagerEmbedded::window_make_current(DisplayServer::WindowID p_window_id) {
+	if (current_window == p_window_id) {
+		return;
+	}
+
+	const GLWindowElement *el = windows.find(p_window_id);
+	if (el == nullptr) {
+		return;
+	}
+
+	const GLWindow &win = el->value();
+	[win.context makeCurrentContext];
+
+	current_window = p_window_id;
+}
+
+void GLManagerEmbedded::swap_buffers() {
+	GLWindow &win = windows[current_window];
+	[win.context flushBuffer];
+
+	static bool last_valid = false;
+	if (!win.is_valid) {
+		if (last_valid) {
+			ERR_PRINT("GLWindow framebuffers are invalid.");
+			last_valid = false;
+		}
+		return;
+	}
+	last_valid = true;
+
+	[CATransaction begin];
+	[CATransaction setDisableActions:YES];
+	win.layer.contents = (__bridge id)win.framebuffers[win.current_fb].surface;
+	[CATransaction commit];
+	win.current_fb = (win.current_fb + 1) % BUFFER_COUNT;
+	GLES3::TextureStorage::system_fbo = win.framebuffers[win.current_fb].fbo;
+}
+
+Error GLManagerEmbedded::initialize() {
+	return framework_loaded ? OK : ERR_CANT_CREATE;
+}
+
+GLManagerEmbedded::GLManagerEmbedded() {
+	NSBundle *framework = [NSBundle bundleWithIdentifier:@"com.apple.opengl"];
+	if ([framework load]) {
+		void *library_handle = dlopen([framework.executablePath UTF8String], RTLD_NOW);
+		if (library_handle) {
+			CGLGetCurrentContext = (CGLGetCurrentContextPtr)dlsym(library_handle, "CGLGetCurrentContext");
+			CGLTexImageIOSurface2D = (CGLTexImageIOSurface2DPtr)dlsym(library_handle, "CGLTexImageIOSurface2D");
+			CGLErrorString = (CGLErrorStringPtr)dlsym(library_handle, "CGLErrorString");
+			framework_loaded = CGLGetCurrentContext && CGLTexImageIOSurface2D && CGLErrorString;
+		}
+	}
+}
+
+GLManagerEmbedded::~GLManagerEmbedded() {
+	release_current();
+}
+
+GODOT_CLANG_WARNING_POP
+
+#endif // MACOS_ENABLED && GLES3_ENABLED

+ 7 - 0
platform/macos/gl_manager_macos_angle.h

@@ -37,10 +37,17 @@
 #include "drivers/egl/egl_manager.h"
 #include "servers/display_server.h"
 
+// Suppress redefinition conflicts
+#define FontVariation __FontVariation
+#define BitMap __BitMap
+
 #import <AppKit/AppKit.h>
 #import <ApplicationServices/ApplicationServices.h>
 #import <CoreVideo/CoreVideo.h>
 
+#undef BitMap
+#undef FontVariation
+
 class GLManagerANGLE_MacOS : public EGLManager {
 private:
 	virtual const char *_get_platform_extension_name() const override;

+ 10 - 0
platform/macos/godot_application.h

@@ -36,5 +36,15 @@
 #import <Foundation/Foundation.h>
 #import <IOKit/hidsystem/ev_keymap.h>
 
+@class GodotApplicationDelegate;
+
 @interface GodotApplication : NSApplication
+
+extern "C" GodotApplication *GodotApp;
+
+@property(readonly, nonatomic) GodotApplicationDelegate *godotDelegate;
+
+- (GodotApplication *)init;
+
+- (void)activateApplication;
 @end

+ 61 - 0
platform/macos/godot_application.mm

@@ -31,9 +31,46 @@
 #import "godot_application.h"
 
 #import "display_server_macos.h"
+#import "godot_application_delegate.h"
+#import "os_macos.h"
+
+GodotApplication *GodotApp = nil;
+
+@interface GodotApplication ()
+- (void)forceUnbundledWindowActivationHackStep1;
+- (void)forceUnbundledWindowActivationHackStep2;
+- (void)forceUnbundledWindowActivationHackStep3;
+@end
 
 @implementation GodotApplication
 
+- (GodotApplication *)init {
+	self = [super init];
+
+	GodotApp = self;
+
+	return self;
+}
+
+- (GodotApplicationDelegate *)godotDelegate {
+	return (GodotApplicationDelegate *)self.delegate;
+}
+
+- (void)activateApplication {
+	[NSApp activateIgnoringOtherApps:YES];
+	NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"];
+	const char *bundled_id = getenv("__CFBundleIdentifier");
+	NSString *nsbundleid_env = [NSString stringWithUTF8String:(bundled_id != nullptr) ? bundled_id : ""];
+	NSString *nsbundleid = [[NSBundle mainBundle] bundleIdentifier];
+	if (nsappname == nil || isatty(STDOUT_FILENO) || isatty(STDIN_FILENO) || isatty(STDERR_FILENO) || ![nsbundleid isEqualToString:nsbundleid_env]) {
+#if DEV_ENABLED
+		if (!OS_MacOS::is_debugger_attached())
+#endif
+			// If the executable is started from terminal or is not bundled, macOS WindowServer won't register and activate app window correctly (menu and title bar are grayed out and input ignored).
+			[self performSelector:@selector(forceUnbundledWindowActivationHackStep1) withObject:nil afterDelay:0.02];
+	}
+}
+
 - (void)mediaKeyEvent:(int)key state:(BOOL)state repeat:(BOOL)repeat {
 	Key keycode = Key::NONE;
 	switch (key) {
@@ -129,4 +166,28 @@
 	}
 }
 
+- (void)forceUnbundledWindowActivationHackStep1 {
+	// Step 1: Switch focus to macOS SystemUIServer process.
+	// Required to perform step 2, TransformProcessType will fail if app is already the in focus.
+	for (NSRunningApplication *app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.systemuiserver"]) {
+		[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
+		break;
+	}
+	[self performSelector:@selector(forceUnbundledWindowActivationHackStep2)
+			   withObject:nil
+			   afterDelay:0.02];
+}
+
+- (void)forceUnbundledWindowActivationHackStep2 {
+	// Step 2: Register app as foreground process.
+	ProcessSerialNumber psn = { 0, kCurrentProcess };
+	(void)TransformProcessType(&psn, kProcessTransformToForegroundApplication);
+	[self performSelector:@selector(forceUnbundledWindowActivationHackStep3) withObject:nil afterDelay:0.02];
+}
+
+- (void)forceUnbundledWindowActivationHackStep3 {
+	// Step 3: Switch focus back to app window.
+	[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
+}
+
 @end

+ 5 - 11
platform/macos/godot_application_delegate.h

@@ -35,18 +35,12 @@
 #import <AppKit/AppKit.h>
 #import <Foundation/Foundation.h>
 
-@interface GodotApplicationDelegate : NSObject <NSUserInterfaceItemSearching, NSApplicationDelegate> {
-	bool high_contrast;
-	bool reduce_motion;
-	bool reduce_transparency;
-	bool voice_over;
-}
+class OS_MacOS_NSApp;
+
+@interface GodotApplicationDelegate : NSObject <NSUserInterfaceItemSearching, NSApplicationDelegate>
+
+- (GodotApplicationDelegate *)initWithOS:(OS_MacOS_NSApp *)os;
 
-- (void)forceUnbundledWindowActivationHackStep1;
-- (void)forceUnbundledWindowActivationHackStep2;
-- (void)forceUnbundledWindowActivationHackStep3;
-- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
-- (void)accessibilityDisplayOptionsChange:(NSNotification *)notification;
 - (bool)getHighContrast;
 - (bool)getReduceMotion;
 - (bool)getReduceTransparency;

+ 47 - 73
platform/macos/godot_application_delegate.mm

@@ -34,7 +34,28 @@
 #import "native_menu_macos.h"
 #import "os_macos.h"
 
-@implementation GodotApplicationDelegate
+#import "main/main.h"
+
+@interface GodotApplicationDelegate ()
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
+- (void)accessibilityDisplayOptionsChange:(NSNotification *)notification;
+@end
+
+@implementation GodotApplicationDelegate {
+	bool high_contrast;
+	bool reduce_motion;
+	bool reduce_transparency;
+	bool voice_over;
+	OS_MacOS_NSApp *os_mac;
+}
+
+- (GodotApplicationDelegate *)initWithOS:(OS_MacOS_NSApp *)os {
+	self = [super init];
+	if (self) {
+		os_mac = os;
+	}
+	return self;
+}
 
 - (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
 	return YES;
@@ -93,30 +114,6 @@
 	}
 }
 
-- (void)forceUnbundledWindowActivationHackStep1 {
-	// Step 1: Switch focus to macOS SystemUIServer process.
-	// Required to perform step 2, TransformProcessType will fail if app is already the in focus.
-	for (NSRunningApplication *app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.systemuiserver"]) {
-		[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
-		break;
-	}
-	[self performSelector:@selector(forceUnbundledWindowActivationHackStep2)
-			   withObject:nil
-			   afterDelay:0.02];
-}
-
-- (void)forceUnbundledWindowActivationHackStep2 {
-	// Step 2: Register app as foreground process.
-	ProcessSerialNumber psn = { 0, kCurrentProcess };
-	(void)TransformProcessType(&psn, kProcessTransformToForegroundApplication);
-	[self performSelector:@selector(forceUnbundledWindowActivationHackStep3) withObject:nil afterDelay:0.02];
-}
-
-- (void)forceUnbundledWindowActivationHackStep3 {
-	// Step 3: Switch focus back to app window.
-	[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
-}
-
 - (void)system_theme_changed:(NSNotification *)notification {
 	DisplayServerMacOS *ds = Object::cast_to<DisplayServerMacOS>(DisplayServer::get_singleton());
 	if (ds) {
@@ -125,22 +122,7 @@
 }
 
 - (void)applicationDidFinishLaunching:(NSNotification *)notification {
-	static_cast<OS_MacOS *>(OS::get_singleton())->start_main();
-}
-
-- (void)activate {
-	[NSApp activateIgnoringOtherApps:YES];
-
-	NSString *nsappname = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"];
-	const char *bundled_id = getenv("__CFBundleIdentifier");
-	NSString *nsbundleid_env = [NSString stringWithUTF8String:(bundled_id != nullptr) ? bundled_id : ""];
-	NSString *nsbundleid = [[NSBundle mainBundle] bundleIdentifier];
-	if (nsappname == nil || isatty(STDOUT_FILENO) || isatty(STDIN_FILENO) || isatty(STDERR_FILENO) || ![nsbundleid isEqualToString:nsbundleid_env]) {
-		// If the executable is started from terminal or is not bundled, macOS WindowServer won't register and activate app window correctly (menu and title bar are grayed out and input ignored).
-		[self performSelector:@selector(forceUnbundledWindowActivationHackStep1) withObject:nil afterDelay:0.02];
-	}
-	[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(system_theme_changed:) name:@"AppleInterfaceThemeChangedNotification" object:nil];
-	[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(system_theme_changed:) name:@"AppleColorPreferencesChangedNotification" object:nil];
+	os_mac->start_main();
 }
 
 static const char *godot_ac_ctx = "gd_accessibility_observer_ctx";
@@ -155,6 +137,9 @@ static const char *godot_ac_ctx = "gd_accessibility_observer_ctx";
 	reduce_transparency = [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceTransparency];
 	voice_over = [[NSWorkspace sharedWorkspace] isVoiceOverEnabled];
 
+	[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(system_theme_changed:) name:@"AppleInterfaceThemeChangedNotification" object:nil];
+	[[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(system_theme_changed:) name:@"AppleColorPreferencesChangedNotification" object:nil];
+
 	return self;
 }
 
@@ -195,10 +180,6 @@ static const char *godot_ac_ctx = "gd_accessibility_observer_ctx";
 }
 
 - (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
-	OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
-	if (!os) {
-		return;
-	}
 	List<String> args;
 	for (NSURL *url in urls) {
 		if ([url isFileURL]) {
@@ -208,33 +189,29 @@ static const char *godot_ac_ctx = "gd_accessibility_observer_ctx";
 		}
 	}
 	if (!args.is_empty()) {
-		if (os->get_main_loop()) {
+		if (os_mac->get_main_loop()) {
 			// Application is already running, open a new instance with the URL/files as command line arguments.
-			os->create_instance(args);
-		} else if (os->get_cmd_argc() == 0) {
+			os_mac->create_instance(args);
+		} else if (os_mac->get_cmd_argc() == 0) {
 			// Application is just started, add to the list of command line arguments and continue.
-			os->set_cmdline_platform_args(args);
+			os_mac->set_cmdline_platform_args(args);
 		}
 	}
 }
 
 - (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames {
-	OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
-	if (!os) {
-		return;
-	}
 	List<String> args;
 	for (NSString *filename in filenames) {
 		NSURL *url = [NSURL URLWithString:filename];
 		args.push_back(String::utf8([url.path UTF8String]));
 	}
 	if (!args.is_empty()) {
-		if (os->get_main_loop()) {
+		if (os_mac->get_main_loop()) {
 			// Application is already running, open a new instance with the URL/files as command line arguments.
-			os->create_instance(args);
-		} else if (os->get_cmd_argc() == 0) {
+			os_mac->create_instance(args);
+		} else if (os_mac->get_cmd_argc() == 0) {
 			// Application is just started, add to the list of command line arguments and continue.
-			os->set_cmdline_platform_args(args);
+			os_mac->set_cmdline_platform_args(args);
 		}
 	}
 }
@@ -244,14 +221,14 @@ static const char *godot_ac_ctx = "gd_accessibility_observer_ctx";
 	if (ds) {
 		ds->mouse_process_popups(true);
 	}
-	if (OS::get_singleton()->get_main_loop()) {
-		OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT);
+	if (os_mac->get_main_loop()) {
+		os_mac->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT);
 	}
 }
 
 - (void)applicationDidBecomeActive:(NSNotification *)notification {
-	if (OS::get_singleton()->get_main_loop()) {
-		OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_IN);
+	if (os_mac->get_main_loop()) {
+		os_mac->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_IN);
 	}
 }
 
@@ -272,29 +249,26 @@ static const char *godot_ac_ctx = "gd_accessibility_observer_ctx";
 }
 
 - (void)applicationWillTerminate:(NSNotification *)notification {
-	OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
-	if (os) {
-		os->cleanup();
-		exit(os->get_exit_code());
-	}
+	os_mac->cleanup();
+	exit(os_mac->get_exit_code());
 }
 
 - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
+	if (os_mac->os_should_terminate()) {
+		return NSTerminateNow;
+	}
+
 	DisplayServerMacOS *ds = Object::cast_to<DisplayServerMacOS>(DisplayServer::get_singleton());
 	if (ds && ds->has_window(DisplayServerMacOS::MAIN_WINDOW_ID)) {
 		ds->send_window_event(ds->get_window(DisplayServerMacOS::MAIN_WINDOW_ID), DisplayServerMacOS::WINDOW_EVENT_CLOSE_REQUEST);
 	}
-	OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
-	if (!os || os->os_should_terminate()) {
-		return NSTerminateNow;
-	}
+
 	return NSTerminateCancel;
 }
 
 - (void)showAbout:(id)sender {
-	OS_MacOS *os = (OS_MacOS *)OS::get_singleton();
-	if (os && os->get_main_loop()) {
-		os->get_main_loop()->notification(MainLoop::NOTIFICATION_WM_ABOUT);
+	if (os_mac->get_main_loop()) {
+		os_mac->get_main_loop()->notification(MainLoop::NOTIFICATION_WM_ABOUT);
 	}
 }
 

+ 7 - 1
platform/macos/godot_content_view.mm

@@ -31,6 +31,7 @@
 #import "godot_content_view.h"
 
 #import "display_server_macos.h"
+#import "godot_window.h"
 #import "key_mapping_macos.h"
 
 #include "main/main.h"
@@ -153,7 +154,12 @@
 // MARK: Backing Layer
 
 - (CALayer *)makeBackingLayer {
-	return [[CAMetalLayer class] layer];
+	CAMetalLayer *layer = [CAMetalLayer new];
+	layer.edgeAntialiasingMask = 0;
+	layer.masksToBounds = NO;
+	layer.presentsWithTransaction = NO;
+	[layer removeAllAnimations];
+	return layer;
 }
 
 - (BOOL)wantsUpdateLayer {

+ 63 - 9
platform/macos/godot_main_macos.mm

@@ -30,9 +30,9 @@
 
 #import "os_macos.h"
 
-#include "main/main.h"
+#import "godot_application.h"
 
-#include <unistd.h>
+#include "main/main.h"
 
 #if defined(SANITIZERS_ENABLED)
 #include <sys/resource.h>
@@ -50,20 +50,74 @@ int main(int argc, char **argv) {
 	setrlimit(RLIMIT_STACK, &stack_lim);
 #endif
 
-	int first_arg = 1;
-	const char *dbg_arg = "-NSDocumentRevisionsDebugMode";
+	LocalVector<char *> args;
+	args.resize(argc);
+	uint32_t argsc = 0;
+
+	int wait_for_debugger = 0; // wait 5 second by default
+	bool is_embedded = false;
+
 	for (int i = 0; i < argc; i++) {
-		if (strcmp(dbg_arg, argv[i]) == 0) {
-			first_arg = i + 2;
+		if (strcmp("-NSDocumentRevisionsDebugMode", argv[i]) == 0) {
+			// remove "-NSDocumentRevisionsDebugMode" and the next argument
+			continue;
+		}
+
+		if (strcmp("--os-debug", argv[i]) == 0) {
+			i++;
+			wait_for_debugger = 5000; // wait 5 seconds by default
+			if (i < argc && strncmp(argv[i], "--", 2) != 0) {
+				wait_for_debugger = atoi(argv[i]);
+			}
+			continue;
+		}
+
+		if (strcmp("--embedded", argv[i]) == 0) {
+			is_embedded = true;
+			continue;
 		}
+
+		args.ptr()[argsc] = argv[i];
+		argsc++;
 	}
 
-	OS_MacOS os(argv[0], argc - first_arg, &argv[first_arg]);
+	uint32_t remaining_args = argsc - 1;
+
+	OS_MacOS *os = nullptr;
+	if (is_embedded) {
+#ifdef DEBUG_ENABLED
+		os = memnew(OS_MacOS_Embedded(args[0], remaining_args, remaining_args > 0 ? &args[1] : nullptr));
+#else
+		WARN_PRINT("Embedded mode is not supported in release builds.");
+		return EXIT_FAILURE;
+#endif
+	} else {
+		os = memnew(OS_MacOS_NSApp(args[0], remaining_args, remaining_args > 0 ? &args[1] : nullptr));
+	}
+
+#ifdef TOOLS_ENABLED
+	if (wait_for_debugger > 0) {
+		os->wait_for_debugger(wait_for_debugger);
+		print_verbose("Continuing execution.");
+	}
+#else
+	if (wait_for_debugger > 0) {
+		WARN_PRINT("--os-debug is not supported in release builds.");
+	}
+#endif
+
+	if (is_embedded) {
+		// No dock icon for the embedded process, as it is hosted in the Godot editor.
+		ProcessSerialNumber psn = { 0, kCurrentProcess };
+		(void)TransformProcessType(&psn, kProcessTransformToBackgroundApplication);
+	}
 
 	// We must override main when testing is enabled.
 	TEST_MAIN_OVERRIDE
 
-	os.run(); // Note: This function will never return.
+	os->run();
+
+	memdelete(os);
 
-	return os.get_exit_code();
+	return os->get_exit_code();
 }

+ 3 - 2
platform/macos/godot_window_delegate.mm

@@ -32,6 +32,7 @@
 
 #import "display_server_macos.h"
 #import "godot_button_view.h"
+#import "godot_content_view.h"
 #import "godot_window.h"
 
 @implementation GodotWindowDelegate
@@ -307,7 +308,7 @@
 	DisplayServerMacOS::WindowData &wd = ds->get_window(window_id);
 
 	if (wd.window_button_view) {
-		[(GodotButtonView *)wd.window_button_view displayButtons];
+		[wd.window_button_view displayButtons];
 	}
 
 	if (ds->mouse_get_mode() == DisplayServer::MOUSE_MODE_CAPTURED) {
@@ -339,7 +340,7 @@
 	DisplayServerMacOS::WindowData &wd = ds->get_window(window_id);
 
 	if (wd.window_button_view) {
-		[(GodotButtonView *)wd.window_button_view displayButtons];
+		[wd.window_button_view displayButtons];
 	}
 
 	wd.focused = false;

+ 94 - 0
platform/macos/macos_quartz_core_spi.h

@@ -0,0 +1,94 @@
+/**************************************************************************/
+/*  macos_quartz_core_spi.h                                               */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+// Copyright 2014 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//     * Redistributions of source code must retain the above copyright
+//       notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//     * Neither the name of the OpenEmu Team nor the
+//       names of its contributors may be used to endorse or promote products
+//       derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY
+// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#import <Foundation/Foundation.h>
+#import <Quartz/Quartz.h>
+
+#include <stdint.h>
+
+// https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/base/cocoa/remote_layer_api.h
+
+// The CAContextID type identifies a CAContext across processes. This is the
+// token that is passed from the process that is sharing the CALayer that it is
+// rendering to the process that will be displaying that CALayer.
+typedef uint32_t CAContextID;
+
+// The CAContext has a static CAContextID which can be sent to another process.
+// When a CALayerHost is created using that CAContextID in another process, the
+// content displayed by that CALayerHost will be the content of the CALayer
+// that is set as the |layer| property on the CAContext.
+@interface CAContext : NSObject
++ (instancetype)contextWithCGSConnection:(CAContextID)contextId options:(NSDictionary *)optionsDict;
+@property(readonly) CAContextID contextId;
+@property(retain) CALayer *layer;
+@end
+
+// The CALayerHost is created in the process that will display the content
+// being rendered by another process. Setting the |contextId| property on
+// an object of this class will make this layer display the content of the
+// CALayer that is set to the CAContext with that CAContextID in the layer
+// sharing process.
+@interface CALayerHost : CALayer
+@property CAContextID contextId;
+@end
+
+// The CGSConnectionID is used to create the CAContext in the process that is
+// going to share the CALayers that it is rendering to another process to
+// display.
+typedef uint32_t CGSConnectionID;
+extern "C" CGSConnectionID CGSMainConnectionID(void);
+
+extern "C" NSString *const kCAContextCIFilterBehavior;

+ 41 - 21
platform/macos/os_macos.h

@@ -40,16 +40,6 @@
 #include "servers/audio_server.h"
 
 class OS_MacOS : public OS_Unix {
-	const char *execpath = nullptr;
-	int argc = 0;
-	char **argv = nullptr;
-
-	id delegate = nullptr;
-	bool should_terminate = false;
-	bool main_stared = false;
-
-	JoypadApple *joypad_apple = nullptr;
-
 #ifdef COREAUDIO_ENABLED
 	AudioDriverCoreAudio audio_driver;
 #endif
@@ -59,10 +49,6 @@ class OS_MacOS : public OS_Unix {
 
 	CrashHandler crash_handler;
 
-	CFRunLoopObserverRef pre_wait_observer = nil;
-
-	MainLoop *main_loop = nullptr;
-
 	List<String> launch_service_args;
 
 	CGFloat _weight_to_ct(int p_weight) const;
@@ -70,11 +56,15 @@ class OS_MacOS : public OS_Unix {
 	String _get_default_fontname(const String &p_font_name) const;
 
 	static _FORCE_INLINE_ String get_framework_executable(const String &p_path);
-	static void pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context);
-
-	void terminate();
 
 protected:
+	const char *execpath = nullptr;
+	int argc = 0;
+	char **argv = nullptr;
+
+	JoypadApple *joypad_apple = nullptr;
+	MainLoop *main_loop = nullptr;
+
 	virtual void initialize_core() override;
 	virtual void initialize() override;
 	virtual void finalize() override;
@@ -132,6 +122,11 @@ public:
 	virtual Vector<String> get_granted_permissions() const override;
 	virtual void revoke_granted_permissions() override;
 
+#ifdef TOOLS_ENABLED
+	static bool is_debugger_attached();
+	void wait_for_debugger(uint32_t p_msec);
+#endif
+
 	virtual bool _check_internal_feature_support(const String &p_feature) override;
 
 	virtual void disable_crash_handler() override;
@@ -142,13 +137,38 @@ public:
 	virtual String get_system_ca_certificates() override;
 	virtual OS::PreferredTextureFormat get_preferred_texture_format() const override;
 
-	void run(); // Runs macOS native event loop.
+	virtual void run() = 0;
+
+	OS_MacOS(const char *p_execpath, int p_argc, char **p_argv);
+};
+
+class OS_MacOS_NSApp : public OS_MacOS {
+	id delegate = nullptr;
+	bool should_terminate = false;
+	bool main_started = false;
+
+	CFRunLoopObserverRef pre_wait_observer = nil;
+
+	void terminate();
+
+public:
 	void start_main(); // Initializes and runs Godot main loop.
-	void activate();
 	void cleanup();
 	bool os_should_terminate() const { return should_terminate; }
 	int get_cmd_argc() const { return argc; }
 
-	OS_MacOS(const char *p_execpath, int p_argc, char **p_argv);
-	~OS_MacOS();
+	virtual void run() override;
+
+	OS_MacOS_NSApp(const char *p_execpath, int p_argc, char **p_argv);
 };
+
+#ifdef DEBUG_ENABLED
+
+class OS_MacOS_Embedded : public OS_MacOS {
+public:
+	virtual void run() override;
+
+	OS_MacOS_Embedded(const char *p_execpath, int p_argc, char **p_argv);
+};
+
+#endif

+ 182 - 73
platform/macos/os_macos.mm

@@ -31,6 +31,9 @@
 #import "os_macos.h"
 
 #import "dir_access_macos.h"
+#ifdef DEBUG_ENABLED
+#import "display_server_embedded.h"
+#endif
 #import "display_server_macos.h"
 #import "godot_application.h"
 #import "godot_application_delegate.h"
@@ -46,32 +49,6 @@
 #include <os/log.h>
 #include <sys/sysctl.h>
 
-void OS_MacOS::pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context) {
-	OS_MacOS *os = static_cast<OS_MacOS *>(OS::get_singleton());
-
-	@autoreleasepool {
-		@try {
-			// Get rid of pending events.
-			DisplayServer *ds = DisplayServer::get_singleton();
-			DisplayServerMacOS *ds_mac = Object::cast_to<DisplayServerMacOS>(ds);
-			if (ds_mac) {
-				ds_mac->_process_events(false);
-			} else if (ds) {
-				ds->process_events();
-			}
-			os->joypad_apple->process_joypads();
-
-			if (Main::iteration()) {
-				os->terminate();
-			}
-		} @catch (NSException *exception) {
-			ERR_PRINT("NSException: " + String::utf8([exception reason].UTF8String));
-		}
-	}
-
-	CFRunLoopWakeUp(CFRunLoopGetCurrent()); // Prevent main loop from sleeping.
-}
-
 void OS_MacOS::initialize() {
 	crash_handler.initialize();
 
@@ -151,6 +128,59 @@ void OS_MacOS::revoke_granted_permissions() {
 	}
 }
 
+#if TOOLS_ENABLED
+
+// Function to check if a debugger is attached to the current process
+bool OS_MacOS::is_debugger_attached() {
+	int mib[4];
+	struct kinfo_proc info{};
+	size_t size = sizeof(info);
+
+	// Initialize the flags so that, if sysctl fails, info.kp_proc.p_flag will be 0.
+	info.kp_proc.p_flag = 0;
+
+	// Initialize mib, which tells sysctl the info we want, in this case we're looking for information
+	// about a specific process ID.
+	mib[0] = CTL_KERN;
+	mib[1] = KERN_PROC;
+	mib[2] = KERN_PROC_PID;
+	mib[3] = getpid();
+
+	if (sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, nullptr, 0) != 0) {
+		perror("sysctl");
+		return false;
+	}
+
+	return (info.kp_proc.p_flag & P_TRACED) != 0;
+}
+
+void OS_MacOS::wait_for_debugger(uint32_t p_msec) {
+	if (p_msec == 0) {
+		return;
+	}
+
+	CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
+	CFTimeInterval wait_time = p_msec / 1000.0;
+
+	NSTimer *timer = [NSTimer timerWithTimeInterval:0.100
+											repeats:YES
+											  block:^(NSTimer *t) {
+												  if (is_debugger_attached() || CFAbsoluteTimeGetCurrent() > start + wait_time) {
+													  [NSApp stopModalWithCode:NSModalResponseContinue];
+													  [t invalidate];
+												  }
+											  }];
+
+	[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSModalPanelRunLoopMode];
+
+	pid_t pid = getpid();
+	alert(vformat("Attach debugger to pid: %d", pid));
+
+	print("continue...");
+}
+
+#endif
+
 void OS_MacOS::initialize_core() {
 	OS_Unix::initialize_core();
 
@@ -854,18 +884,53 @@ OS::PreferredTextureFormat OS_MacOS::get_preferred_texture_format() const {
 	return PREFERRED_TEXTURE_FORMAT_S3TC_BPTC;
 }
 
-void OS_MacOS::run() {
+OS_MacOS::OS_MacOS(const char *p_execpath, int p_argc, char **p_argv) {
+	execpath = p_execpath;
+	argc = p_argc;
+	argv = p_argv;
+
+	if (is_sandboxed()) {
+		// Load security-scoped bookmarks, request access, remove stale or invalid bookmarks.
+		NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
+		NSMutableArray *new_bookmarks = [[NSMutableArray alloc] init];
+		for (id bookmark in bookmarks) {
+			NSError *error = nil;
+			BOOL isStale = NO;
+			NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
+			if (!error && !isStale) {
+				if ([url startAccessingSecurityScopedResource]) {
+					[new_bookmarks addObject:bookmark];
+				}
+			}
+		}
+		[[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"];
+	}
+
+	Vector<Logger *> loggers;
+	loggers.push_back(memnew(MacOSTerminalLogger));
+	_set_logger(memnew(CompositeLogger(loggers)));
+
+#ifdef COREAUDIO_ENABLED
+	AudioDriverManager::add_driver(&audio_driver);
+#endif
+
+	DisplayServerMacOS::register_macos_driver();
+}
+
+// MARK: - OS_MacOS_NSApp
+
+void OS_MacOS_NSApp::run() {
 	[NSApp run];
 }
 
-void OS_MacOS::start_main() {
+void OS_MacOS_NSApp::start_main() {
 	Error err;
 	@autoreleasepool {
 		err = Main::setup(execpath, argc, argv);
 	}
 
 	if (err == OK) {
-		main_stared = true;
+		main_started = true;
 
 		int ret;
 		@autoreleasepool {
@@ -876,7 +941,29 @@ void OS_MacOS::start_main() {
 				@autoreleasepool {
 					main_loop->initialize();
 				}
-				pre_wait_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, &pre_wait_observer_cb, nullptr);
+				DisplayServer *ds = DisplayServer::get_singleton();
+				DisplayServerMacOS *ds_mac = Object::cast_to<DisplayServerMacOS>(ds);
+
+				pre_wait_observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
+					@autoreleasepool {
+						@try {
+							if (ds_mac) {
+								ds_mac->_process_events(false);
+							} else if (ds) {
+								ds->process_events();
+							}
+							joypad_apple->process_joypads();
+
+							if (Main::iteration()) {
+								terminate();
+							}
+						} @catch (NSException *exception) {
+							ERR_PRINT("NSException: " + String::utf8([exception reason].UTF8String));
+						}
+					}
+
+					CFRunLoopWakeUp(CFRunLoopGetCurrent()); // Prevent main loop from sleeping.
+				});
 				CFRunLoopAddObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes);
 				return;
 			}
@@ -892,11 +979,7 @@ void OS_MacOS::start_main() {
 	terminate();
 }
 
-void OS_MacOS::activate() {
-	[delegate activate];
-}
-
-void OS_MacOS::terminate() {
+void OS_MacOS_NSApp::terminate() {
 	if (pre_wait_observer) {
 		CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes);
 		CFRelease(pre_wait_observer);
@@ -907,50 +990,19 @@ void OS_MacOS::terminate() {
 	[NSApp terminate:nil];
 }
 
-void OS_MacOS::cleanup() {
+void OS_MacOS_NSApp::cleanup() {
 	if (main_loop) {
 		main_loop->finalize();
 	}
-	if (main_stared) {
+	if (main_started) {
 		@autoreleasepool {
 			Main::cleanup();
 		}
 	}
 }
 
-OS_MacOS::OS_MacOS(const char *p_execpath, int p_argc, char **p_argv) {
-	execpath = p_execpath;
-	argc = p_argc;
-	argv = p_argv;
-	if (is_sandboxed()) {
-		// Load security-scoped bookmarks, request access, remove stale or invalid bookmarks.
-		NSArray *bookmarks = [[NSUserDefaults standardUserDefaults] arrayForKey:@"sec_bookmarks"];
-		NSMutableArray *new_bookmarks = [[NSMutableArray alloc] init];
-		for (id bookmark in bookmarks) {
-			NSError *error = nil;
-			BOOL isStale = NO;
-			NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope relativeToURL:nil bookmarkDataIsStale:&isStale error:&error];
-			if (!error && !isStale) {
-				if ([url startAccessingSecurityScopedResource]) {
-					[new_bookmarks addObject:bookmark];
-				}
-			}
-		}
-		[[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"];
-	}
-
-	main_loop = nullptr;
-
-	Vector<Logger *> loggers;
-	loggers.push_back(memnew(MacOSTerminalLogger));
-	_set_logger(memnew(CompositeLogger(loggers)));
-
-#ifdef COREAUDIO_ENABLED
-	AudioDriverManager::add_driver(&audio_driver);
-#endif
-
-	DisplayServerMacOS::register_macos_driver();
-
+OS_MacOS_NSApp::OS_MacOS_NSApp(const char *p_execpath, int p_argc, char **p_argv) :
+		OS_MacOS(p_execpath, p_argc, p_argv) {
 	// Implicitly create shared NSApplication instance.
 	[GodotApplication sharedApplication];
 
@@ -964,12 +1016,69 @@ OS_MacOS::OS_MacOS(const char *p_execpath, int p_argc, char **p_argv) {
 	NSMenu *main_menu = [[NSMenu alloc] initWithTitle:@""];
 	[NSApp setMainMenu:main_menu];
 
-	delegate = [[GodotApplicationDelegate alloc] init];
+	delegate = [[GodotApplicationDelegate alloc] initWithOS:this];
 	ERR_FAIL_NULL(delegate);
 	[NSApp setDelegate:delegate];
 	[NSApp registerUserInterfaceItemSearchHandler:delegate];
 }
 
-OS_MacOS::~OS_MacOS() {
-	// NOP
+// MARK: - OS_MacOS_Embedded
+
+#ifdef DEBUG_ENABLED
+
+void OS_MacOS_Embedded::run() {
+	CFRunLoopGetCurrent();
+
+	@autoreleasepool {
+		Error err = Main::setup(execpath, argc, argv);
+		if (err != OK) {
+			if (err == ERR_HELP) {
+				return set_exit_code(EXIT_SUCCESS);
+			}
+			return set_exit_code(EXIT_FAILURE);
+		}
+	}
+
+	int ret;
+	@autoreleasepool {
+		ret = Main::start();
+	}
+
+	DisplayServerEmbedded *ds = Object::cast_to<DisplayServerEmbedded>(DisplayServer::get_singleton());
+	if (!ds) {
+		ERR_FAIL_MSG("DisplayServerEmbedded is not initialized.");
+	}
+
+	if (ds && ret == EXIT_SUCCESS && main_loop) {
+		@autoreleasepool {
+			main_loop->initialize();
+		}
+
+		while (true) {
+			@autoreleasepool {
+				@try {
+					ds->process_events();
+
+					if (Main::iteration()) {
+						break;
+					}
+
+					CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, 0);
+				} @catch (NSException *exception) {
+					ERR_PRINT("NSException: " + String::utf8([exception reason].UTF8String));
+				}
+			}
+		}
+
+		main_loop->finalize();
+	}
+
+	Main::cleanup();
 }
+
+OS_MacOS_Embedded::OS_MacOS_Embedded(const char *p_execpath, int p_argc, char **p_argv) :
+		OS_MacOS(p_execpath, p_argc, p_argv) {
+	DisplayServerEmbedded::register_embedded_driver();
+}
+
+#endif