diff --git a/core/input/input_enums.h b/core/input/input_enums.h index d4efb3d81dc..27508cce464 100644 --- a/core/input/input_enums.h +++ b/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, diff --git a/core/input/input_event.h b/core/input/input_event.h index 8bc9c5cd6c4..62b89bf0fdf 100644 --- a/core/input/input_event.h +++ b/core/input/input_event.h @@ -89,6 +89,8 @@ public: virtual bool accumulate(const Ref &p_event) { return false; } + virtual InputEventType get_type() const { return InputEventType::INVALID; } + InputEvent() {} }; @@ -202,6 +204,8 @@ public: static Ref 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 &p_event) override; + InputEventType get_type() const final override { return InputEventType::MOUSE_MOTION; } + InputEventMouseMotion() {} }; @@ -333,6 +341,8 @@ public: static Ref 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 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 &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(); }; diff --git a/core/input/input_event_codec.cpp b/core/input/input_event_codec.cpp new file mode 100644 index 00000000000..365af582f3a --- /dev/null +++ b/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(b); +} + +uint8_t encode_key_modifier_state(Ref 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 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 &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 &r_event) { + const uint8_t *data = p_data.ptr(); + DEV_ASSERT(static_cast(*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 &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 &r_event) { + const uint8_t *data = p_data.ptr(); + DEV_ASSERT(static_cast(*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 button_mask = (MouseButtonMask)*data; + r_event->set_button_mask(button_mask); + + return OK; +} + +void encode_input_event_mouse_motion(const Ref &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 &r_event) { + Input *input = Input::get_singleton(); + + const uint8_t *data = p_data.ptr(); + DEV_ASSERT(static_cast(*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 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 &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 &r_event) { + const uint8_t *data = p_data.ptr(); + DEV_ASSERT(static_cast(*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 &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 &r_event) { + const uint8_t *data = p_data.ptr(); + DEV_ASSERT(static_cast(*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 &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 &r_event) { + const uint8_t *data = p_data.ptr(); + DEV_ASSERT(static_cast(*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 &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 &r_event) { + const uint8_t *data = p_data.ptr(); + DEV_ASSERT(static_cast(*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 &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 &r_event) { + const uint8_t *data = p_data.ptr(); + + switch (static_cast(*data)) { + case InputEventType::KEY: { + Ref event; + event.instantiate(); + decode_input_event_key(p_data, event); + r_event = event; + } break; + case InputEventType::MOUSE_BUTTON: { + Ref event; + event.instantiate(); + decode_input_event_mouse_button(p_data, event); + r_event = event; + } break; + case InputEventType::MOUSE_MOTION: { + Ref event; + event.instantiate(); + decode_input_event_mouse_motion(p_data, event); + r_event = event; + } break; + case InputEventType::JOY_BUTTON: { + Ref event; + event.instantiate(); + decode_input_event_joypad_button(p_data, event); + r_event = event; + } break; + case InputEventType::JOY_MOTION: { + Ref event; + event.instantiate(); + decode_input_event_joypad_motion(p_data, event); + r_event = event; + } break; + case InputEventType::PAN_GESTURE: { + Ref event; + event.instantiate(); + decode_input_event_gesture_pan(p_data, event); + r_event = event; + } break; + case InputEventType::MAGNIFY_GESTURE: { + Ref 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(*data))); + } break; + } +} diff --git a/core/input/input_event_codec.h b/core/input/input_event_codec.h new file mode 100644 index 00000000000..438ca91ac30 --- /dev/null +++ b/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 &p_event, PackedByteArray &r_data); +void decode_input_event(const PackedByteArray &p_data, Ref &r_event); + +void encode_input_event_key(const Ref &p_event, PackedByteArray &r_data); +void encode_input_event_mouse_button(const Ref &p_event, PackedByteArray &r_data); +void encode_input_event_mouse_motion(const Ref &p_event, PackedByteArray &r_data); +void encode_input_event_joypad_button(const Ref &p_event, PackedByteArray &r_data); +void encode_input_event_joypad_motion(const Ref &p_event, PackedByteArray &r_data); +void encode_input_event_gesture_pan(const Ref &p_event, PackedByteArray &r_data); +void encode_input_event_gesture_magnify(const Ref &p_event, PackedByteArray &r_data); diff --git a/drivers/apple/joypad_apple.h b/drivers/apple/joypad_apple.h index 10f88789493..1ea46ce9a66 100644 --- a/drivers/apple/joypad_apple.h +++ b/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(JoyAxis::MAX) < 32, "JoyAxis::MAX must be less than 32"); double axis_value[(int)JoyAxis::MAX]; GameController(int p_joy_id, GCController *p_controller); diff --git a/drivers/apple/joypad_apple.mm b/drivers/apple/joypad_apple.mm index fced2f31dcb..38dfc5aca8c 100644 --- a/drivers/apple/joypad_apple.mm +++ b/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 &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) { diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index bad407aa0a6..6d35c1adf2c 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/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 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); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 23c5860cb5e..9f6235aed8e 100644 --- a/editor/editor_node.cpp +++ b/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(); diff --git a/editor/plugins/embedded_process.cpp b/editor/plugins/embedded_process.cpp index 8aae1d88cfc..bde64c6aa9b 100644 --- a/editor/plugins/embedded_process.cpp +++ b/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 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() { diff --git a/editor/plugins/embedded_process.h b/editor/plugins/embedded_process.h index 1464a8496e2..bb98147d7bb 100644 --- a/editor/plugins/embedded_process.h +++ b/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 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 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(); + 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; - void set_window_size(const Size2i p_window_size); - void set_keep_aspect(bool p_keep_aspect); - void queue_update_embedded_process(); - - 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; }; diff --git a/editor/plugins/game_view_plugin.cpp b/editor/plugins/game_view_plugin.cpp index 978f44d1c81..e4fd744c7de 100644 --- a/editor/plugins/game_view_plugin.cpp +++ b/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 &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 &r_argumen List::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 p_debugger, WindowWrapper *p_wrapper) { +GameView::GameView(Ref 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 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 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 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 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 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 p_layout) { +void GameViewPluginBase::set_window_layout(Ref p_layout) { game_view->set_window_layout(p_layout); } -void GameViewPlugin::get_window_layout(Ref p_layout) { +void GameViewPluginBase::get_window_layout(Ref p_layout) { game_view->get_window_layout(p_layout); } + +void GameViewPluginBase::setup(Ref 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(); - -#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 +GameViewPluginBase::GameViewPluginBase() { +} + +GameViewPlugin::GameViewPlugin() : + GameViewPluginBase() { +#ifndef ANDROID_ENABLED + Ref game_view_debugger; + game_view_debugger.instantiate(); + EmbeddedProcess *embedded_process = memnew(EmbeddedProcess); + setup(game_view_debugger, embedded_process); +#endif } diff --git a/editor/plugins/game_view_plugin.h b/editor/plugins/game_view_plugin.h index fc77a9f48c1..a9209838577 100644 --- a/editor/plugins/game_view_plugin.h +++ b/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 p_layout); void get_window_layout(Ref p_layout); - GameView(Ref p_debugger, WindowWrapper *p_wrapper); + GameView(Ref 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 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 p_layout) override; virtual void get_window_layout(Ref p_layout) override; #endif // ANDROID_ENABLED + GameViewPluginBase(); +}; +class GameViewPlugin : public GameViewPluginBase { + GDCLASS(GameViewPlugin, GameViewPluginBase); + +public: GameViewPlugin(); }; diff --git a/platform/macos/SCsub b/platform/macos/SCsub index e3ad3146e79..ef220efbde7 100644 --- a/platform/macos/SCsub +++ b/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"]: diff --git a/platform/macos/detect.py b/platform/macos/detect.py index 0a08471f2d7..62f92719990 100644 --- a/platform/macos/detect.py +++ b/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"]) diff --git a/platform/macos/display_server_embedded.h b/platform/macos/display_server_embedded.h new file mode 100644 index 00000000000..18a3638a136 --- /dev/null +++ b/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 window_attached_instance_id; + + HashMap window_event_callbacks; + HashMap window_resize_callbacks; + HashMap input_event_callbacks; + HashMap 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 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 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 &p_event); + void send_input_event(const Ref &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 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 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 &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(); +}; diff --git a/platform/macos/display_server_embedded.mm b/platform/macos/display_server_embedded.mm new file mode 100644 index 00000000000..7308b97b96e --- /dev/null +++ b/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 DisplayServerEmbedded::get_rendering_drivers_func() { + Vector 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 DisplayServerEmbedded::mouse_get_button_state() const { + BitField 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 &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 &p_event) { + Ref 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 &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 &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 DisplayServerEmbedded::get_window_list() const { + Vector 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 &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 +} diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index e21da1499ea..b899a52546e 100644 --- a/platform/macos/display_server_macos.h +++ b/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 @@ -60,8 +61,18 @@ #import #import +@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 mpath; @@ -241,6 +252,12 @@ private: void initialize_tts() const; + struct EmbeddedProcessData { + const EmbeddedProcessMacOS *process; + CALayer *layer_host = nil; + }; + HashMap 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; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index bbe94449279..4e99a7a9ec4 100644 --- a/platform/macos/display_server_macos.mm +++ b/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 *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::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(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(); diff --git a/platform/macos/editor/embedded_game_view_plugin.h b/platform/macos/editor/embedded_game_view_plugin.h new file mode 100644 index 00000000000..7b078cba8ed --- /dev/null +++ b/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 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(); diff --git a/platform/macos/editor/embedded_game_view_plugin.mm b/platform/macos/editor/embedded_game_view_plugin.mm new file mode 100644 index 00000000000..9bfd308a0d6 --- /dev/null +++ b/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 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(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 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 debugger; + debugger.instantiate(embedded_process); + + setup(debugger, embedded_process); +} + +extern "C" GameViewPluginBase *get_game_view_plugin() { + return memnew(GameViewPluginMacOS); +} diff --git a/platform/macos/editor/embedded_process_macos.h b/platform/macos/editor/embedded_process_macos.h new file mode 100644 index 00000000000..01d34842489 --- /dev/null +++ b/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 &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(); +}; diff --git a/platform/macos/editor/embedded_process_macos.mm b/platform/macos/editor/embedded_process_macos.mm new file mode 100644 index 00000000000..82189b0d72d --- /dev/null +++ b/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(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 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 &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(); + } +} diff --git a/platform/macos/embedded_debugger.h b/platform/macos/embedded_debugger.h new file mode 100644 index 00000000000..028c9542b9c --- /dev/null +++ b/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 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 +}; diff --git a/platform/macos/embedded_debugger.mm b/platform/macos/embedded_debugger.mm new file mode 100644 index 00000000000..46b55c1b5c7 --- /dev/null +++ b/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 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 event; + decode_input_event(data, event); + + { + Ref e = event; + if (e.is_valid()) { + input->set_mouse_position(e->get_position()); + } + } + + { + Ref e = event; + if (e.is_valid()) { + input->set_mouse_position(e->get_position()); + } + } + + { + Ref 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(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 diff --git a/platform/macos/embedded_gl_manager.h b/platform/macos/embedded_gl_manager.h new file mode 100644 index 00000000000..64e4fc18f73 --- /dev/null +++ b/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 +#import +#import + +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 windows; + typedef RBMap::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 diff --git a/platform/macos/embedded_gl_manager.mm b/platform/macos/embedded_gl_manager.mm new file mode 100644 index 00000000000..19d8f8ec810 --- /dev/null +++ b/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 +#include + +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 diff --git a/platform/macos/gl_manager_macos_angle.h b/platform/macos/gl_manager_macos_angle.h index 933ec1646c5..4498ef02a6e 100644 --- a/platform/macos/gl_manager_macos_angle.h +++ b/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 #import #import +#undef BitMap +#undef FontVariation + class GLManagerANGLE_MacOS : public EGLManager { private: virtual const char *_get_platform_extension_name() const override; diff --git a/platform/macos/godot_application.h b/platform/macos/godot_application.h index 44db57eeb17..022c02f324e 100644 --- a/platform/macos/godot_application.h +++ b/platform/macos/godot_application.h @@ -36,5 +36,15 @@ #import #import +@class GodotApplicationDelegate; + @interface GodotApplication : NSApplication + +extern "C" GodotApplication *GodotApp; + +@property(readonly, nonatomic) GodotApplicationDelegate *godotDelegate; + +- (GodotApplication *)init; + +- (void)activateApplication; @end diff --git a/platform/macos/godot_application.mm b/platform/macos/godot_application.mm index 900d2ca99b0..e9663461d4d 100644 --- a/platform/macos/godot_application.mm +++ b/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 diff --git a/platform/macos/godot_application_delegate.h b/platform/macos/godot_application_delegate.h index eeac379466f..34bd2d878d5 100644 --- a/platform/macos/godot_application_delegate.h +++ b/platform/macos/godot_application_delegate.h @@ -35,18 +35,12 @@ #import #import -@interface GodotApplicationDelegate : NSObject { - bool high_contrast; - bool reduce_motion; - bool reduce_transparency; - bool voice_over; -} +class OS_MacOS_NSApp; + +@interface GodotApplicationDelegate : NSObject + +- (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; diff --git a/platform/macos/godot_application_delegate.mm b/platform/macos/godot_application_delegate.mm index e7ece517d26..8bf10561673 100644 --- a/platform/macos/godot_application_delegate.mm +++ b/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(DisplayServer::get_singleton()); if (ds) { @@ -125,22 +122,7 @@ } - (void)applicationDidFinishLaunching:(NSNotification *)notification { - static_cast(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 *)urls { - OS_MacOS *os = (OS_MacOS *)OS::get_singleton(); - if (!os) { - return; - } List 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 *)filenames { - OS_MacOS *os = (OS_MacOS *)OS::get_singleton(); - if (!os) { - return; - } List 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(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); } } diff --git a/platform/macos/godot_content_view.mm b/platform/macos/godot_content_view.mm index 69374cb9e35..42891090eeb 100644 --- a/platform/macos/godot_content_view.mm +++ b/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 { diff --git a/platform/macos/godot_main_macos.mm b/platform/macos/godot_main_macos.mm index 9a5b9fd7c21..7fdd9173fee 100644 --- a/platform/macos/godot_main_macos.mm +++ b/platform/macos/godot_main_macos.mm @@ -30,9 +30,9 @@ #import "os_macos.h" -#include "main/main.h" +#import "godot_application.h" -#include +#include "main/main.h" #if defined(SANITIZERS_ENABLED) #include @@ -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 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(); - return os.get_exit_code(); + memdelete(os); + + return os->get_exit_code(); } diff --git a/platform/macos/godot_window_delegate.mm b/platform/macos/godot_window_delegate.mm index cc9fa0c4b6e..35585813dac 100644 --- a/platform/macos/godot_window_delegate.mm +++ b/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; diff --git a/platform/macos/macos_quartz_core_spi.h b/platform/macos/macos_quartz_core_spi.h new file mode 100644 index 00000000000..6dd4e4351ea --- /dev/null +++ b/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 +#import + +#include + +// 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; diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index 2bb4f9cba7a..f222610a1f6 100644 --- a/platform/macos/os_macos.h +++ b/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 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 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 diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index f205c29c3c3..0f364f379a9 100644 --- a/platform/macos/os_macos.mm +++ b/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 #include -void OS_MacOS::pre_wait_observer_cb(CFRunLoopObserverRef p_observer, CFRunLoopActivity p_activiy, void *p_context) { - OS_MacOS *os = static_cast(OS::get_singleton()); - - @autoreleasepool { - @try { - // Get rid of pending events. - DisplayServer *ds = DisplayServer::get_singleton(); - DisplayServerMacOS *ds_mac = Object::cast_to(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,74 +884,11 @@ OS::PreferredTextureFormat OS_MacOS::get_preferred_texture_format() const { return PREFERRED_TEXTURE_FORMAT_S3TC_BPTC; } -void OS_MacOS::run() { - [NSApp run]; -} - -void OS_MacOS::start_main() { - Error err; - @autoreleasepool { - err = Main::setup(execpath, argc, argv); - } - - if (err == OK) { - main_stared = true; - - int ret; - @autoreleasepool { - ret = Main::start(); - } - if (ret == EXIT_SUCCESS) { - if (main_loop) { - @autoreleasepool { - main_loop->initialize(); - } - pre_wait_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, &pre_wait_observer_cb, nullptr); - CFRunLoopAddObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); - return; - } - } else { - set_exit_code(EXIT_FAILURE); - } - } else if (err == ERR_HELP) { // Returned by --help and --version, so success. - set_exit_code(EXIT_SUCCESS); - } else { - set_exit_code(EXIT_FAILURE); - } - - terminate(); -} - -void OS_MacOS::activate() { - [delegate activate]; -} - -void OS_MacOS::terminate() { - if (pre_wait_observer) { - CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); - CFRelease(pre_wait_observer); - pre_wait_observer = nil; - } - - should_terminate = true; - [NSApp terminate:nil]; -} - -void OS_MacOS::cleanup() { - if (main_loop) { - main_loop->finalize(); - } - if (main_stared) { - @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"]; @@ -939,8 +906,6 @@ OS_MacOS::OS_MacOS(const char *p_execpath, int p_argc, char **p_argv) { [[NSUserDefaults standardUserDefaults] setObject:new_bookmarks forKey:@"sec_bookmarks"]; } - main_loop = nullptr; - Vector loggers; loggers.push_back(memnew(MacOSTerminalLogger)); _set_logger(memnew(CompositeLogger(loggers))); @@ -950,7 +915,94 @@ OS_MacOS::OS_MacOS(const char *p_execpath, int p_argc, char **p_argv) { #endif DisplayServerMacOS::register_macos_driver(); +} +// MARK: - OS_MacOS_NSApp + +void OS_MacOS_NSApp::run() { + [NSApp run]; +} + +void OS_MacOS_NSApp::start_main() { + Error err; + @autoreleasepool { + err = Main::setup(execpath, argc, argv); + } + + if (err == OK) { + main_started = true; + + int ret; + @autoreleasepool { + ret = Main::start(); + } + if (ret == EXIT_SUCCESS) { + if (main_loop) { + @autoreleasepool { + main_loop->initialize(); + } + DisplayServer *ds = DisplayServer::get_singleton(); + DisplayServerMacOS *ds_mac = Object::cast_to(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; + } + } else { + set_exit_code(EXIT_FAILURE); + } + } else if (err == ERR_HELP) { // Returned by --help and --version, so success. + set_exit_code(EXIT_SUCCESS); + } else { + set_exit_code(EXIT_FAILURE); + } + + terminate(); +} + +void OS_MacOS_NSApp::terminate() { + if (pre_wait_observer) { + CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), pre_wait_observer, kCFRunLoopCommonModes); + CFRelease(pre_wait_observer); + pre_wait_observer = nil; + } + + should_terminate = true; + [NSApp terminate:nil]; +} + +void OS_MacOS_NSApp::cleanup() { + if (main_loop) { + main_loop->finalize(); + } + if (main_started) { + @autoreleasepool { + Main::cleanup(); + } + } +} + +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(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