diff options
Diffstat (limited to 'platform')
121 files changed, 6407 insertions, 3717 deletions
diff --git a/platform/android/detect.py b/platform/android/detect.py index a4ac87f723..0accacb679 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -115,7 +115,8 @@ def configure(env): if env["android_arch"] == "x86_64": if get_platform(env["ndk_platform"]) < 21: print( - "WARNING: android_arch=x86_64 is not supported by ndk_platform lower than android-21; setting ndk_platform=android-21" + "WARNING: android_arch=x86_64 is not supported by ndk_platform lower than android-21; setting" + " ndk_platform=android-21" ) env["ndk_platform"] = "android-21" env["ARCH"] = "arch-x86_64" @@ -136,7 +137,8 @@ def configure(env): elif env["android_arch"] == "arm64v8": if get_platform(env["ndk_platform"]) < 21: print( - "WARNING: android_arch=arm64v8 is not supported by ndk_platform lower than android-21; setting ndk_platform=android-21" + "WARNING: android_arch=arm64v8 is not supported by ndk_platform lower than android-21; setting" + " ndk_platform=android-21" ) env["ndk_platform"] = "android-21" env["ARCH"] = "arch-arm64" @@ -231,7 +233,10 @@ def configure(env): env.Append(CPPDEFINES=[("__ANDROID_API__", str(get_platform(env["ndk_platform"])))]) env.Append( - CCFLAGS="-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden -fno-strict-aliasing".split() + CCFLAGS=( + "-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -fvisibility=hidden" + " -fno-strict-aliasing".split() + ) ) env.Append(CPPDEFINES=["NO_STATVFS", "GLES_ENABLED"]) diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 7193519a52..3aa2fb5451 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -36,9 +36,6 @@ #include "java_godot_wrapper.h" #include "os_android.h" -#if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" -#endif #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" #include "platform/android/vulkan/vulkan_context_android.h" @@ -155,12 +152,12 @@ bool DisplayServerAndroid::screen_is_touchscreen(int p_screen) const { return true; } -void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, int p_max_length, int p_cursor_start, int p_cursor_end) { +void DisplayServerAndroid::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_length, int p_cursor_start, int p_cursor_end) { GodotIOJavaWrapper *godot_io_java = OS_Android::get_singleton()->get_godot_io_java(); ERR_FAIL_COND(!godot_io_java); if (godot_io_java->has_vk()) { - godot_io_java->show_vk(p_existing_text, p_max_length, p_cursor_start, p_cursor_end); + godot_io_java->show_vk(p_existing_text, p_multiline, p_max_length, p_cursor_start, p_cursor_end); } else { ERR_PRINT("Virtual keyboard not available"); } diff --git a/platform/android/display_server_android.h b/platform/android/display_server_android.h index 4cae52fa76..5cdc69ee83 100644 --- a/platform/android/display_server_android.h +++ b/platform/android/display_server_android.h @@ -113,7 +113,7 @@ public: virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const; virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const; - virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); + virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); virtual void virtual_keyboard_hide(); virtual int virtual_keyboard_get_height() const; diff --git a/platform/android/export/export.cpp b/platform/android/export/export.cpp index 0213094c60..37da1dbd4d 100644 --- a/platform/android/export/export.cpp +++ b/platform/android/export/export.cpp @@ -44,6 +44,7 @@ #include "editor/editor_log.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" +#include "main/splash.gen.h" #include "platform/android/export/gradle_export_util.h" #include "platform/android/logo.gen.h" #include "platform/android/plugin/godot_plugin_config.h" @@ -200,6 +201,9 @@ static const char *android_perms[] = { nullptr }; +static const char *SPLASH_IMAGE_EXPORT_PATH = "res/drawable/splash.png"; +static const char *SPLASH_BG_COLOR_PATH = "res/drawable/splash_bg_color.png"; + struct LauncherIcon { const char *export_path; int dimensions; @@ -453,7 +457,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { String name; bool first = true; for (int i = 0; i < basename.length(); i++) { - CharType c = basename[i]; + char32_t c = basename[i]; if (c >= '0' && c <= '9' && first) { continue; } @@ -484,7 +488,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { int segments = 0; bool first = true; for (int i = 0; i < pname.length(); i++) { - CharType c = pname[i]; + char32_t c = pname[i]; if (first && c == '.') { if (r_error) { *r_error = TTR("Package segments must be of non-zero length."); @@ -588,7 +592,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { zipfi.tmz_date.tm_hour = time.hour; zipfi.tmz_date.tm_mday = date.day; zipfi.tmz_date.tm_min = time.min; - zipfi.tmz_date.tm_mon = date.month; + zipfi.tmz_date.tm_mon = date.month - 1; // tm_mon is zero indexed zipfi.tmz_date.tm_sec = time.sec; zipfi.tmz_date.tm_year = date.year; zipfi.dosDate = 0; @@ -723,7 +727,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { return OK; } - static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { + static Error save_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { APKExportData *ed = (APKExportData *)p_userdata; String dst_path = p_path.replace_first("res://", "assets/"); @@ -731,7 +735,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { return OK; } - static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { + static Error ignore_apk_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { return OK; } @@ -768,6 +772,30 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } } + void _write_tmp_manifest(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, bool p_debug) { + String manifest_text = + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + " xmlns:tools=\"http://schemas.android.com/tools\">\n"; + + manifest_text += _get_screen_sizes_tag(p_preset); + manifest_text += _get_gles_tag(); + + Vector<String> perms; + _get_permissions(p_preset, p_give_internet, perms); + for (int i = 0; i < perms.size(); i++) { + manifest_text += vformat(" <uses-permission android:name=\"%s\" />\n", perms.get(i)); + } + + manifest_text += _get_xr_features_tag(p_preset); + manifest_text += _get_instrumentation_tag(p_preset); + String plugins_names = get_plugins_names(get_enabled_plugins(p_preset)); + manifest_text += _get_application_tag(p_preset, plugins_names); + manifest_text += "</manifest>\n"; + String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release")); + store_string_at_path(manifest_path, manifest_text); + } + void _fix_manifest(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_manifest, bool p_give_internet) { // Leaving the unused types commented because looking these constants up // again later would be annoying @@ -849,7 +877,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { if (string_flags & UTF8_FLAG) { } else { uint32_t len = decode_uint16(&p_manifest[string_at]); - Vector<CharType> ucstring; + Vector<char32_t> ucstring; ucstring.resize(len + 1); for (uint32_t j = 0; j < len; j++) { uint16_t c = decode_uint16(&p_manifest[string_at + 2 + 2 * j]); @@ -1310,7 +1338,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } else { String str; for (uint32_t i = 0; i < len; i++) { - CharType c = decode_uint16(&p_bytes[offset + i * 2]); + char32_t c = decode_uint16(&p_bytes[offset + i * 2]); if (c == 0) { break; } @@ -1409,6 +1437,18 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { //printf("end\n"); } + void _load_image_data(const Ref<Image> &p_splash_image, Vector<uint8_t> &p_data) { + Vector<uint8_t> png_buffer; + Error err = PNGDriverCommon::image_to_png(p_splash_image, png_buffer); + if (err == OK) { + p_data.resize(png_buffer.size()); + memcpy(p_data.ptrw(), png_buffer.ptr(), p_data.size()); + } else { + String err_str = String("Failed to convert splash image to png."); + WARN_PRINT(err_str.utf8().get_data()); + } + } + void _process_launcher_icons(const String &p_file_name, const Ref<Image> &p_source_image, int dimension, Vector<uint8_t> &p_data) { Ref<Image> working_image = p_source_image; @@ -1428,6 +1468,35 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } } + void load_splash_refs(Ref<Image> &splash_image, Ref<Image> &splash_bg_color_image) { + // TODO: Figure out how to handle remaining boot splash parameters (e.g: fullsize, filter) + String project_splash_path = ProjectSettings::get_singleton()->get("application/boot_splash/image"); + + if (!project_splash_path.empty()) { + splash_image.instance(); + const Error err = ImageLoader::load_image(project_splash_path, splash_image); + if (err) { + splash_image.unref(); + } + } + + if (splash_image.is_null()) { + // Use the default + splash_image = Ref<Image>(memnew(Image(boot_splash_png))); + } + + // Setup the splash bg color + bool bg_color_valid; + Color bg_color = ProjectSettings::get_singleton()->get("application/boot_splash/bg_color", &bg_color_valid); + if (!bg_color_valid) { + bg_color = boot_splash_bg_color; + } + + splash_bg_color_image.instance(); + splash_bg_color_image->create(splash_image->get_width(), splash_image->get_height(), false, splash_image->get_format()); + splash_bg_color_image->fill(bg_color); + } + void load_icon_refs(const Ref<EditorExportPreset> &p_preset, Ref<Image> &icon, Ref<Image> &foreground, Ref<Image> &background) { String project_icon_path = ProjectSettings::get_singleton()->get("application/config/icon"); @@ -1455,13 +1524,34 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } void store_image(const LauncherIcon launcher_icon, const Vector<uint8_t> &data) { - String img_path = launcher_icon.export_path; - img_path = img_path.insert(0, "res://android/build/"); + store_image(launcher_icon.export_path, data); + } + + void store_image(const String &export_path, const Vector<uint8_t> &data) { + String img_path = export_path.insert(0, "res://android/build/"); store_file_at_path(img_path, data); } - void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &main_image, - const Ref<Image> &foreground, const Ref<Image> &background) { + void _copy_icons_to_gradle_project(const Ref<EditorExportPreset> &p_preset, + const Ref<Image> &splash_image, + const Ref<Image> &splash_bg_color_image, + const Ref<Image> &main_image, + const Ref<Image> &foreground, + const Ref<Image> &background) { + // Store the splash image + if (splash_image.is_valid() && !splash_image->empty()) { + Vector<uint8_t> data; + _load_image_data(splash_image, data); + store_image(SPLASH_IMAGE_EXPORT_PATH, data); + } + + // Store the splash bg color image + if (splash_bg_color_image.is_valid() && !splash_bg_color_image->empty()) { + Vector<uint8_t> data; + _load_image_data(splash_bg_color_image, data); + store_image(SPLASH_BG_COLOR_PATH, data); + } + // Prepare images to be resized for the icons. If some image ends up being uninitialized, // the default image from the export template will be used. @@ -1501,7 +1591,7 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { } public: - typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total); + typedef Error (*EditorExportSaveFunction)(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key); public: virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) override { @@ -1530,6 +1620,7 @@ public: r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.apk"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "custom_template/use_custom_build"), false)); + r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "custom_template/export_format", PROPERTY_HINT_ENUM, "Export APK,Export AAB"), 0)); Vector<PluginConfig> plugins_configs = get_plugins(); for (int i = 0; i < plugins_configs.size(); i++) { @@ -1950,6 +2041,13 @@ public: } } + if (int(p_preset->get("custom_template/export_format")) == 1 && /*AAB*/ + !bool(p_preset->get("custom_template/use_custom_build"))) { + valid = false; + err += TTR("\"Export AAB\" is only valid when \"Use Custom Build\" is enabled."); + err += "\n"; + } + r_error = err; return valid; } @@ -1957,6 +2055,7 @@ public: virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override { List<String> list; list.push_back("apk"); + list.push_back("aab"); return list; } @@ -1983,7 +2082,21 @@ public: return have_plugins_changed || first_build; } - Error get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags) { + String get_apk_expansion_fullpath(const Ref<EditorExportPreset> &p_preset, const String &p_path) { + int version_code = p_preset->get("version/code"); + String package_name = p_preset->get("package/unique_name"); + String apk_file_name = "main." + itos(version_code) + "." + get_package_name(package_name) + ".obb"; + String fullpath = p_path.get_base_dir().plus_file(apk_file_name); + return fullpath; + } + + Error save_apk_expansion_file(const Ref<EditorExportPreset> &p_preset, const String &p_path) { + String fullpath = get_apk_expansion_fullpath(p_preset, p_path); + Error err = save_pack(p_preset, fullpath); + return err; + } + + void get_command_line_flags(const Ref<EditorExportPreset> &p_preset, const String &p_path, int p_flags, Vector<uint8_t> &r_command_line_flags) { String cmdline = p_preset->get("command_line/extra_args"); Vector<String> command_line_strings = cmdline.strip_edges().split(" "); for (int i = 0; i < command_line_strings.size(); i++) { @@ -1997,17 +2110,8 @@ public: bool apk_expansion = p_preset->get("apk_expansion/enable"); if (apk_expansion) { - int version_code = p_preset->get("version/code"); - String package_name = p_preset->get("package/unique_name"); - String apk_file_name = "main." + itos(version_code) + "." + get_package_name(package_name) + ".obb"; - String fullpath = p_path.get_base_dir().plus_file(apk_file_name); + String fullpath = get_apk_expansion_fullpath(p_preset, p_path); String apk_expansion_public_key = p_preset->get("apk_expansion/public_key"); - Error err = save_pack(p_preset, fullpath); - - if (err != OK) { - EditorNode::add_io_error("Could not write expansion package file: " + apk_file_name); - return err; - } command_line_strings.push_back("--use_apk_expansion"); command_line_strings.push_back("--apk_expansion_md5"); @@ -2053,6 +2157,92 @@ public: copymem(&r_command_line_flags.write[base + 4], command_line_argument.ptr(), length); } } + } + + Error sign_apk(const Ref<EditorExportPreset> &p_preset, bool p_debug, String apk_path, EditorProgress ep) { + String release_keystore = p_preset->get("keystore/release"); + String release_username = p_preset->get("keystore/release_user"); + String release_password = p_preset->get("keystore/release_password"); + + String jarsigner = EditorSettings::get_singleton()->get("export/android/jarsigner"); + if (!FileAccess::exists(jarsigner)) { + EditorNode::add_io_error("'jarsigner' could not be found.\nPlease supply a path in the Editor Settings.\nThe resulting APK is unsigned."); + return OK; + } + + String keystore; + String password; + String user; + if (p_debug) { + keystore = p_preset->get("keystore/debug"); + password = p_preset->get("keystore/debug_password"); + user = p_preset->get("keystore/debug_user"); + + if (keystore.empty()) { + keystore = EditorSettings::get_singleton()->get("export/android/debug_keystore"); + password = EditorSettings::get_singleton()->get("export/android/debug_keystore_pass"); + user = EditorSettings::get_singleton()->get("export/android/debug_keystore_user"); + } + + if (ep.step("Signing debug APK...", 103)) { + return ERR_SKIP; + } + + } else { + keystore = release_keystore; + password = release_password; + user = release_username; + + if (ep.step("Signing release APK...", 103)) { + return ERR_SKIP; + } + } + + if (!FileAccess::exists(keystore)) { + EditorNode::add_io_error("Could not find keystore, unable to export."); + return ERR_FILE_CANT_OPEN; + } + + List<String> args; + args.push_back("-digestalg"); + args.push_back("SHA-256"); + args.push_back("-sigalg"); + args.push_back("SHA256withRSA"); + String tsa_url = EditorSettings::get_singleton()->get("export/android/timestamping_authority_url"); + if (tsa_url != "") { + args.push_back("-tsa"); + args.push_back(tsa_url); + } + args.push_back("-verbose"); + args.push_back("-keystore"); + args.push_back(keystore); + args.push_back("-storepass"); + args.push_back(password); + args.push_back(apk_path); + args.push_back(user); + int retval; + OS::get_singleton()->execute(jarsigner, args, true, NULL, NULL, &retval); + if (retval) { + EditorNode::add_io_error("'jarsigner' returned with error #" + itos(retval)); + return ERR_CANT_CREATE; + } + + if (ep.step("Verifying APK...", 104)) { + return ERR_SKIP; + } + + args.clear(); + args.push_back("-verify"); + args.push_back("-keystore"); + args.push_back(keystore); + args.push_back(apk_path); + args.push_back("-verbose"); + + OS::get_singleton()->execute(jarsigner, args, true, NULL, NULL, &retval); + if (retval) { + EditorNode::add_io_error("'jarsigner' verification of APK failed. Make sure to use a jarsigner from OpenJDK 8."); + return ERR_CANT_CREATE; + } return OK; } @@ -2060,10 +2250,20 @@ public: ExportNotifier notifier(*this, p_preset, p_debug, p_path, p_flags); String src_apk; + Error err; EditorProgress ep("export", "Exporting for Android", 105, true); bool use_custom_build = bool(p_preset->get("custom_template/use_custom_build")); + int export_format = int(p_preset->get("custom_template/export_format")); + bool p_give_internet = p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG); + bool _signed = p_preset->get("package/signed"); + bool apk_expansion = p_preset->get("apk_expansion/enable"); + Vector<String> enabled_abis = get_enabled_abis(p_preset); + + Ref<Image> splash_image; + Ref<Image> splash_bg_color_image; + load_splash_refs(splash_image, splash_bg_color_image); Ref<Image> main_image; Ref<Image> foreground; @@ -2071,39 +2271,81 @@ public: load_icon_refs(p_preset, main_image, foreground, background); + Vector<uint8_t> command_line_flags; + // Write command line flags into the command_line_flags variable. + get_command_line_flags(p_preset, p_path, p_flags, command_line_flags); + + if (export_format == 1) { + if (!p_path.ends_with(".aab")) { + EditorNode::get_singleton()->show_warning(TTR("Invalid filename! Android App Bundle requires the *.aab extension.")); + return ERR_UNCONFIGURED; + } + if (apk_expansion) { + EditorNode::get_singleton()->show_warning(TTR("APK Expansion not compatible with Android App Bundle.")); + return ERR_UNCONFIGURED; + } + } + if (export_format == 0 && !p_path.ends_with(".apk")) { + EditorNode::get_singleton()->show_warning( + TTR("Invalid filename! Android APK requires the *.apk extension.")); + return ERR_UNCONFIGURED; + } + if (export_format > 1 || export_format < 0) { + EditorNode::add_io_error("Unsupported export format!\n"); + return ERR_UNCONFIGURED; //TODO: is this the right error? + } + if (use_custom_build) { - //re-generate build.gradle and AndroidManifest.xml - { //test that installed build version is alright - FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::READ); - if (!f) { - EditorNode::get_singleton()->show_warning(TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); - return ERR_UNCONFIGURED; - } - String version = f->get_line().strip_edges(); - if (version != VERSION_FULL_CONFIG) { - EditorNode::get_singleton()->show_warning(vformat(TTR("Android build version mismatch:\n Template installed: %s\n Godot Version: %s\nPlease reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); - return ERR_UNCONFIGURED; - } + //test that installed build version is alright + FileAccessRef f = FileAccess::open("res://android/.build_version", FileAccess::READ); + if (!f) { + EditorNode::get_singleton()->show_warning(TTR("Trying to build from a custom built template, but no version info for it exists. Please reinstall from the 'Project' menu.")); + return ERR_UNCONFIGURED; + } + String version = f->get_line().strip_edges(); + if (version != VERSION_FULL_CONFIG) { + EditorNode::get_singleton()->show_warning(vformat(TTR("Android build version mismatch:\n Template installed: %s\n Godot Version: %s\nPlease reinstall Android build template from 'Project' menu."), version, VERSION_FULL_CONFIG)); + return ERR_UNCONFIGURED; } + String sdk_path = EDITOR_GET("export/android/custom_build_sdk_path"); + ERR_FAIL_COND_V_MSG(sdk_path == "", ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/custom_build_sdk_path'."); // TODO: should we use "package/name" or "application/config/name"? String project_name = get_project_name(p_preset->get("package/name")); - // instead of calling _fix_resources - Error err = _create_project_name_strings_files(p_preset, project_name); + err = _create_project_name_strings_files(p_preset, project_name); //project name localization. if (err != OK) { EditorNode::add_io_error("Unable to overwrite res://android/build/res/*.xml files with project name"); } - // Copies the project icon files into the appropriate Gradle project directory - _copy_icons_to_gradle_project(p_preset, main_image, foreground, background); - - //build project if custom build is enabled - String sdk_path = EDITOR_GET("export/android/custom_build_sdk_path"); - - ERR_FAIL_COND_V_MSG(sdk_path == "", ERR_UNCONFIGURED, "Android SDK path must be configured in Editor Settings at 'export/android/custom_build_sdk_path'."); + // Copies the project icon files into the appropriate Gradle project directory. + _copy_icons_to_gradle_project(p_preset, splash_image, splash_bg_color_image, main_image, foreground, background); + // Write an AndroidManifest.xml file into the Gradle project directory. + _write_tmp_manifest(p_preset, p_give_internet, p_debug); + + //stores all the project files inside the Gradle project directory. Also includes all ABIs + if (!apk_expansion) { + DirAccess *da_res = DirAccess::create(DirAccess::ACCESS_RESOURCES); + if (da_res->dir_exists("res://android/build/assets")) { + DirAccess *da_assets = DirAccess::open("res://android/build/assets"); + da_assets->erase_contents_recursive(); + da_res->remove("res://android/build/assets"); + } + err = export_project_files(p_preset, rename_and_store_file_in_gradle_project, NULL, ignore_so_file); + if (err != OK) { + EditorNode::add_io_error("Could not export project files to gradle project\n"); + return err; + } + } else { + err = save_apk_expansion_file(p_preset, p_path); + if (err != OK) { + EditorNode::add_io_error("Could not write expansion package file!"); + return err; + } + } + store_file_at_path("res://android/build/assets/_cl_", command_line_flags); OS::get_singleton()->set_environment("ANDROID_HOME", sdk_path); //set and overwrite if required - String build_command; + #ifdef WINDOWS_ENABLED build_command = "gradlew.bat"; #else @@ -2111,10 +2353,12 @@ public: #endif String build_path = ProjectSettings::get_singleton()->get_resource_path().plus_file("android/build"); - build_command = build_path.plus_file(build_command); String package_name = get_package_name(p_preset->get("package/unique_name")); + String version_code = itos(p_preset->get("version/code")); + String version_name = p_preset->get("version/name"); + String enabled_abi_string = String("|").join(enabled_abis); Vector<PluginConfig> enabled_plugins = get_enabled_plugins(p_preset); String local_plugins_binaries = get_plugins_binaries(BINARY_TYPE_LOCAL, enabled_plugins); @@ -2126,8 +2370,20 @@ public: if (clean_build_required) { cmdline.push_back("clean"); } - cmdline.push_back("build"); + + String build_type = p_debug ? "Debug" : "Release"; + if (export_format == 1) { + String bundle_build_command = vformat("bundle%s", build_type); + cmdline.push_back(bundle_build_command); + } else if (export_format == 0) { + String apk_build_command = vformat("assemble%s", build_type); + cmdline.push_back(apk_build_command); + } + cmdline.push_back("-Pexport_package_name=" + package_name); // argument to specify the package name. + cmdline.push_back("-Pexport_version_code=" + version_code); // argument to specify the version code. + cmdline.push_back("-Pexport_version_name=" + version_name); // argument to specify the version name. + cmdline.push_back("-Pexport_enabled_abis=" + enabled_abi_string); // argument to specify enabled ABIs. cmdline.push_back("-Pplugins_local_binaries=" + local_plugins_binaries); // argument to specify the list of plugins local dependencies. cmdline.push_back("-Pplugins_remote_binaries=" + remote_plugins_binaries); // argument to specify the list of plugins remote dependencies. cmdline.push_back("-Pplugins_maven_repos=" + custom_maven_repos); // argument to specify the list of custom maven repos for the plugins dependencies. @@ -2145,35 +2401,54 @@ public: EditorNode::get_singleton()->show_warning(TTR("Building of Android project failed, check output for the error.\nAlternatively visit docs.godotengine.org for Android build documentation.")); return ERR_CANT_CREATE; } - if (p_debug) { - src_apk = build_path.plus_file("build/outputs/apk/debug/android_debug.apk"); - } else { - src_apk = build_path.plus_file("build/outputs/apk/release/android_release.apk"); + + List<String> copy_args; + String copy_command; + if (export_format == 1) { + copy_command = vformat("copyAndRename%sAab", build_type); + } else if (export_format == 0) { + copy_command = vformat("copyAndRename%sApk", build_type); } - if (!FileAccess::exists(src_apk)) { - EditorNode::get_singleton()->show_warning(TTR("No build apk generated at: ") + "\n" + src_apk); + copy_args.push_back(copy_command); + + copy_args.push_back("-p"); // argument to specify the start directory. + copy_args.push_back(build_path); // start directory. + + String export_filename = p_path.get_file(); + String export_path = p_path.get_base_dir(); + + copy_args.push_back("-Pexport_path=file:" + export_path); + copy_args.push_back("-Pexport_filename=" + export_filename); + + int copy_result = EditorNode::get_singleton()->execute_and_show_output(TTR("Moving output"), build_command, copy_args); + if (copy_result != 0) { + EditorNode::get_singleton()->show_warning(TTR("Unable to copy and rename export file, check gradle project directory for outputs.")); return ERR_CANT_CREATE; } - - } else { + if (_signed) { + err = sign_apk(p_preset, p_debug, p_path, ep); + if (err != OK) { + return err; + } + } + return OK; + } + // This is the start of the Legacy build system + if (p_debug) + src_apk = p_preset->get("custom_template/debug"); + else + src_apk = p_preset->get("custom_template/release"); + src_apk = src_apk.strip_edges(); + if (src_apk == "") { if (p_debug) { - src_apk = p_preset->get("custom_template/debug"); + src_apk = find_export_template("android_debug.apk"); } else { - src_apk = p_preset->get("custom_template/release"); + src_apk = find_export_template("android_release.apk"); } - - src_apk = src_apk.strip_edges(); if (src_apk == "") { - if (p_debug) { - src_apk = find_export_template("android_debug.apk"); - } else { - src_apk = find_export_template("android_release.apk"); - } - if (src_apk == "") { - EditorNode::add_io_error("Package not found: " + src_apk); - return ERR_FILE_NOT_FOUND; - } + EditorNode::add_io_error("Package not found: " + src_apk); + return ERR_FILE_NOT_FOUND; } } @@ -2210,23 +2485,13 @@ public: zipFile unaligned_apk = zipOpen2(tmp_unaligned_path.utf8().get_data(), APPEND_STATUS_CREATE, nullptr, &io2); - bool _signed = p_preset->get("package/signed"); String cmdline = p_preset->get("command_line/extra_args"); String version_name = p_preset->get("version/name"); String package_name = p_preset->get("package/unique_name"); - bool apk_expansion = p_preset->get("apk_expansion/enable"); String apk_expansion_pkey = p_preset->get("apk_expansion/public_key"); - String release_keystore = p_preset->get("keystore/release"); - String release_username = p_preset->get("keystore/release_user"); - String release_password = p_preset->get("keystore/release_password"); - - Vector<String> enabled_abis = get_enabled_abis(p_preset); - - // Prepare images to be resized for the icons. If some image ends up being uninitialized, the default image from the export template will be used. - Vector<String> invalid_abis(enabled_abis); while (ret == UNZ_OK) { //get filename @@ -2247,31 +2512,37 @@ public: unzCloseCurrentFile(pkg); //write - if (file == "AndroidManifest.xml") { - _fix_manifest(p_preset, data, p_flags & (DEBUG_FLAG_DUMB_CLIENT | DEBUG_FLAG_REMOTE_DEBUG)); + _fix_manifest(p_preset, data, p_give_internet); + } + if (file == "resources.arsc") { + _fix_resources(p_preset, data); } - if (!use_custom_build) { - if (file == "resources.arsc") { - _fix_resources(p_preset, data); - } + // Process the splash image + if (file == SPLASH_IMAGE_EXPORT_PATH && splash_image.is_valid() && !splash_image->empty()) { + _load_image_data(splash_image, data); + } - for (int i = 0; i < icon_densities_count; ++i) { - if (main_image.is_valid() && !main_image->empty()) { - if (file == launcher_icons[i].export_path) { - _process_launcher_icons(file, main_image, launcher_icons[i].dimensions, data); - } + // Process the splash bg color image + if (file == SPLASH_BG_COLOR_PATH && splash_bg_color_image.is_valid() && !splash_bg_color_image->empty()) { + _load_image_data(splash_bg_color_image, data); + } + + for (int i = 0; i < icon_densities_count; ++i) { + if (main_image.is_valid() && !main_image->empty()) { + if (file == launcher_icons[i].export_path) { + _process_launcher_icons(file, main_image, launcher_icons[i].dimensions, data); } - if (foreground.is_valid() && !foreground->empty()) { - if (file == launcher_adaptive_icon_foregrounds[i].export_path) { - _process_launcher_icons(file, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data); - } + } + if (foreground.is_valid() && !foreground->empty()) { + if (file == launcher_adaptive_icon_foregrounds[i].export_path) { + _process_launcher_icons(file, foreground, launcher_adaptive_icon_foregrounds[i].dimensions, data); } - if (background.is_valid() && !background->empty()) { - if (file == launcher_adaptive_icon_backgrounds[i].export_path) { - _process_launcher_icons(file, background, launcher_adaptive_icon_backgrounds[i].dimensions, data); - } + } + if (background.is_valid() && !background->empty()) { + if (file == launcher_adaptive_icon_backgrounds[i].export_path) { + _process_launcher_icons(file, background, launcher_adaptive_icon_backgrounds[i].dimensions, data); } } } @@ -2330,18 +2601,26 @@ public: if (ep.step("Adding files...", 1)) { CLEANUP_AND_RETURN(ERR_SKIP); } - Error err = OK; + err = OK; if (p_flags & DEBUG_FLAG_DUMB_CLIENT) { APKExportData ed; ed.ep = &ep; ed.apk = unaligned_apk; err = export_project_files(p_preset, ignore_apk_file, &ed, save_apk_so); - } else if (!apk_expansion) { - APKExportData ed; - ed.ep = &ep; - ed.apk = unaligned_apk; - err = export_project_files(p_preset, save_apk_file, &ed, save_apk_so); + } else { + if (apk_expansion) { + err = save_apk_expansion_file(p_preset, p_path); + if (err != OK) { + EditorNode::add_io_error("Could not write expansion package file!"); + return err; + } + } else { + APKExportData ed; + ed.ep = &ep; + ed.apk = unaligned_apk; + err = export_project_files(p_preset, save_apk_file, &ed, save_apk_so); + } } if (err != OK) { @@ -2350,10 +2629,6 @@ public: CLEANUP_AND_RETURN(ERR_SKIP); } - Vector<uint8_t> command_line_flags; - // Write command line flags into the command_line_flags variable. - err = get_command_line_flags(p_preset, p_path, p_flags, command_line_flags); - zip_fileinfo zipfi = get_zip_fileinfo(); zipOpenNewFileInZip(unaligned_apk, "assets/_cl_", @@ -2375,84 +2650,9 @@ public: } if (_signed) { - String jarsigner = EditorSettings::get_singleton()->get("export/android/jarsigner"); - if (!FileAccess::exists(jarsigner)) { - EditorNode::add_io_error("'jarsigner' could not be found.\nPlease supply a path in the Editor Settings.\nThe resulting APK is unsigned."); - CLEANUP_AND_RETURN(OK); - } - - String keystore; - String password; - String user; - if (p_debug) { - keystore = p_preset->get("keystore/debug"); - password = p_preset->get("keystore/debug_password"); - user = p_preset->get("keystore/debug_user"); - - if (keystore.empty()) { - keystore = EditorSettings::get_singleton()->get("export/android/debug_keystore"); - password = EditorSettings::get_singleton()->get("export/android/debug_keystore_pass"); - user = EditorSettings::get_singleton()->get("export/android/debug_keystore_user"); - } - - if (ep.step("Signing debug APK...", 103)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - - } else { - keystore = release_keystore; - password = release_password; - user = release_username; - - if (ep.step("Signing release APK...", 103)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - } - - if (!FileAccess::exists(keystore)) { - EditorNode::add_io_error("Could not find keystore, unable to export."); - CLEANUP_AND_RETURN(ERR_FILE_CANT_OPEN); - } - - List<String> args; - args.push_back("-digestalg"); - args.push_back("SHA-256"); - args.push_back("-sigalg"); - args.push_back("SHA256withRSA"); - String tsa_url = EditorSettings::get_singleton()->get("export/android/timestamping_authority_url"); - if (tsa_url != "") { - args.push_back("-tsa"); - args.push_back(tsa_url); - } - args.push_back("-verbose"); - args.push_back("-keystore"); - args.push_back(keystore); - args.push_back("-storepass"); - args.push_back(password); - args.push_back(tmp_unaligned_path); - args.push_back(user); - int retval; - OS::get_singleton()->execute(jarsigner, args, true, nullptr, nullptr, &retval); - if (retval) { - EditorNode::add_io_error("'jarsigner' returned with error #" + itos(retval)); - CLEANUP_AND_RETURN(ERR_CANT_CREATE); - } - - if (ep.step("Verifying APK...", 104)) { - CLEANUP_AND_RETURN(ERR_SKIP); - } - - args.clear(); - args.push_back("-verify"); - args.push_back("-keystore"); - args.push_back(keystore); - args.push_back(tmp_unaligned_path); - args.push_back("-verbose"); - - OS::get_singleton()->execute(jarsigner, args, true, nullptr, nullptr, &retval); - if (retval) { - EditorNode::add_io_error("'jarsigner' verification of APK failed. Make sure to use a jarsigner from OpenJDK 8."); - CLEANUP_AND_RETURN(ERR_CANT_CREATE); + err = sign_apk(p_preset, p_debug, tmp_unaligned_path, ep); + if (err != OK) { + CLEANUP_AND_RETURN(err); } } diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 622860c307..95f870bc35 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -99,7 +99,7 @@ Error store_string_at_path(const String &p_path, const String &p_data) { // It is used by the export_project_files method to save all the asset files into the gradle project. // It's functionality mirrors that of the method save_apk_file. // This method will be called ONLY when custom build is enabled. -Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { +Error rename_and_store_file_in_gradle_project(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { String dst_path = p_path.replace_first("res://", "res://android/build/assets/"); Error err = store_file_at_path(dst_path, p_data); return err; @@ -142,4 +142,104 @@ Error _create_project_name_strings_files(const Ref<EditorExportPreset> &p_preset return OK; } +String bool_to_string(bool v) { + return v ? "true" : "false"; +} + +String _get_gles_tag() { + bool min_gles3 = ProjectSettings::get_singleton()->get("rendering/quality/driver/driver_name") == "GLES3" && + !ProjectSettings::get_singleton()->get("rendering/quality/driver/fallback_to_gles2"); + return min_gles3 ? " <uses-feature android:glEsVersion=\"0x00030000\" android:required=\"true\" />\n" : ""; +} + +String _get_screen_sizes_tag(const Ref<EditorExportPreset> &p_preset) { + String manifest_screen_sizes = " <supports-screens \n tools:node=\"replace\""; + String sizes[] = { "small", "normal", "large", "xlarge" }; + size_t num_sizes = sizeof(sizes) / sizeof(sizes[0]); + for (size_t i = 0; i < num_sizes; i++) { + String feature_name = vformat("screen/support_%s", sizes[i]); + String feature_support = bool_to_string(p_preset->get(feature_name)); + String xml_entry = vformat("\n android:%sScreens=\"%s\"", sizes[i], feature_support); + manifest_screen_sizes += xml_entry; + } + manifest_screen_sizes += " />\n"; + return manifest_screen_sizes; +} + +String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset) { + String manifest_xr_features; + bool uses_xr = (int)(p_preset->get("xr_features/xr_mode")) == 1; + if (uses_xr) { + int dof_index = p_preset->get("xr_features/degrees_of_freedom"); // 0: none, 1: 3dof and 6dof, 2: 6dof + if (dof_index == 1) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vr.headtracking\" android:required=\"false\" android:version=\"1\" />\n"; + } else if (dof_index == 2) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"android.hardware.vr.headtracking\" android:required=\"true\" android:version=\"1\" />\n"; + } + int hand_tracking_index = p_preset->get("xr_features/hand_tracking"); // 0: none, 1: optional, 2: required + if (hand_tracking_index == 1) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"false\" />\n"; + } else if (hand_tracking_index == 2) { + manifest_xr_features += " <uses-feature tools:node=\"replace\" android:name=\"oculus.software.handtracking\" android:required=\"true\" />\n"; + } + } + return manifest_xr_features; +} + +String _get_instrumentation_tag(const Ref<EditorExportPreset> &p_preset) { + String package_name = p_preset->get("package/unique_name"); + String manifest_instrumentation_text = vformat( + " <instrumentation\n" + " tools:node=\"replace\"\n" + " android:name=\".GodotInstrumentation\"\n" + " android:icon=\"@mipmap/icon\"\n" + " android:label=\"@string/godot_project_name_string\"\n" + " android:targetPackage=\"%s\" />\n", + package_name); + return manifest_instrumentation_text; +} + +String _get_plugins_tag(const String &plugins_names) { + if (!plugins_names.empty()) { + return vformat(" <meta-data tools:node=\"replace\" android:name=\"plugins\" android:value=\"%s\" />\n", plugins_names); + } else { + return " <meta-data tools:node=\"remove\" android:name=\"plugins\" />\n"; + } +} + +String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) { + bool uses_xr = (int)(p_preset->get("xr_features/xr_mode")) == 1; + String orientation = (int)(p_preset->get("screen/orientation")) == 1 ? "portrait" : "landscape"; + String manifest_activity_text = vformat( + " <activity android:name=\"com.godot.game.GodotApp\" " + "tools:replace=\"android:screenOrientation\" " + "android:screenOrientation=\"%s\">\n", + orientation); + if (uses_xr) { + String focus_awareness = bool_to_string(p_preset->get("xr_features/focus_awareness")); + manifest_activity_text += vformat(" <meta-data tools:node=\"replace\" android:name=\"com.oculus.vr.focusaware\" android:value=\"%s\" />\n", focus_awareness); + } else { + manifest_activity_text += " <meta-data tools:node=\"remove\" android:name=\"com.oculus.vr.focusaware\" />\n"; + } + manifest_activity_text += " </activity>\n"; + return manifest_activity_text; +} + +String _get_application_tag(const Ref<EditorExportPreset> &p_preset, const String &plugins_names) { + bool uses_xr = (int)(p_preset->get("xr_features/xr_mode")) == 1; + String manifest_application_text = + " <application android:label=\"@string/godot_project_name_string\"\n" + " android:allowBackup=\"false\" tools:ignore=\"GoogleAppIndexingWarning\"\n" + " android:icon=\"@mipmap/icon\">)\n\n" + " <meta-data tools:node=\"remove\" android:name=\"xr_mode_metadata_name\" />\n"; + + manifest_application_text += _get_plugins_tag(plugins_names); + if (uses_xr) { + manifest_application_text += " <meta-data tools:node=\"replace\" android:name=\"com.samsung.android.vr.application.mode\" android:value=\"vr_only\" />\n"; + } + manifest_application_text += _get_activity_tag(p_preset); + manifest_application_text += " </application>\n"; + return manifest_application_text; +} + #endif //GODOT_GRADLE_EXPORT_UTIL_H diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index 48c09552c1..e94681659c 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -38,7 +38,7 @@ <activity android:name=".GodotApp" android:label="@string/godot_project_name_string" - android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" + android:theme="@style/GodotAppSplashTheme" android:launchMode="singleTask" android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 19202d2310..ceacfec9e1 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -80,8 +80,15 @@ android { ignoreAssetsPattern "!.svn:!.git:!.ds_store:!*.scc:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~" } + ndk { + String[] export_abi_list = getExportEnabledABIs() + abiFilters export_abi_list + } + // Feel free to modify the application id to your own. applicationId getExportPackageName() + versionCode getExportVersionCode() + versionName getExportVersionName() minSdkVersion versions.minSdk targetSdkVersion versions.targetSdk } @@ -123,3 +130,27 @@ android { } } } + +task copyAndRenameDebugApk(type: Copy) { + from "$buildDir/outputs/apk/debug/android_debug.apk" + into getExportPath() + rename "android_debug.apk", getExportFilename() +} + +task copyAndRenameReleaseApk(type: Copy) { + from "$buildDir/outputs/apk/release/android_release.apk" + into getExportPath() + rename "android_release.apk", getExportFilename() +} + +task copyAndRenameDebugAab(type: Copy) { + from "$buildDir/outputs/bundle/debug/build-debug.aab" + into getExportPath() + rename "build-debug.aab", getExportFilename() +} + +task copyAndRenameReleaseAab(type: Copy) { + from "$buildDir/outputs/bundle/release/build-release.aab" + into getExportPath() + rename "build-release.aab", getExportFilename() +} diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index acfdef531e..d1176e6196 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -28,8 +28,55 @@ ext.getExportPackageName = { -> return appId } +ext.getExportVersionCode = { -> + String versionCode = project.hasProperty("export_version_code") ? project.property("export_version_code") : "" + if (versionCode == null || versionCode.isEmpty()) { + versionCode = "1" + } + return Integer.parseInt(versionCode) +} + +ext.getExportVersionName = { -> + String versionName = project.hasProperty("export_version_name") ? project.property("export_version_name") : "" + if (versionName == null || versionName.isEmpty()) { + versionName = "1.0" + } + return versionName +} + final String PLUGIN_VALUE_SEPARATOR_REGEX = "\\|" +// get the list of ABIs the project should be exported to +ext.getExportEnabledABIs = { -> + String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""; + if (enabledABIs == null || enabledABIs.isEmpty()) { + enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|" + } + Set<String> exportAbiFilter = []; + for (String abi_name : enabledABIs.split(PLUGIN_VALUE_SEPARATOR_REGEX)) { + if (!abi_name.trim().isEmpty()){ + exportAbiFilter.add(abi_name); + } + } + return exportAbiFilter; +} + +ext.getExportPath = { + String exportPath = project.hasProperty("export_path") ? project.property("export_path") : "" + if (exportPath == null || exportPath.isEmpty()) { + exportPath = "." + } + return exportPath +} + +ext.getExportFilename = { + String exportFilename = project.hasProperty("export_filename") ? project.property("export_filename") : "" + if (exportFilename == null || exportFilename.isEmpty()) { + exportFilename = "godot_android" + } + return exportFilename +} + /** * Parse the project properties for the 'plugins_maven_repos' property and return the list * of maven repos. diff --git a/platform/android/java/app/res/drawable/splash.png b/platform/android/java/app/res/drawable/splash.png Binary files differnew file mode 100644 index 0000000000..7bddd4325a --- /dev/null +++ b/platform/android/java/app/res/drawable/splash.png diff --git a/platform/android/java/app/res/drawable/splash_bg_color.png b/platform/android/java/app/res/drawable/splash_bg_color.png Binary files differnew file mode 100644 index 0000000000..004b6fd508 --- /dev/null +++ b/platform/android/java/app/res/drawable/splash_bg_color.png diff --git a/platform/android/java/app/res/drawable/splash_drawable.xml b/platform/android/java/app/res/drawable/splash_drawable.xml new file mode 100644 index 0000000000..2794a40817 --- /dev/null +++ b/platform/android/java/app/res/drawable/splash_drawable.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:drawable="@drawable/splash_bg_color" /> + + <item> + <bitmap + android:gravity="center" + android:src="@drawable/splash" /> + </item> + +</layer-list> diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml new file mode 100644 index 0000000000..26912538d3 --- /dev/null +++ b/platform/android/java/app/res/values/themes.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"/> + + <style name="GodotAppSplashTheme" parent="@style/GodotAppMainTheme"> + <item name="android:windowBackground">@drawable/splash_drawable</item> + </style> +</resources> diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java index 1af5950cbe..51df70969e 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -32,9 +32,16 @@ package com.godot.game; import org.godotengine.godot.FullScreenGodotApp; +import android.os.Bundle; + /** * Template activity for Godot Android custom builds. * Feel free to extend and modify this class for your custom logic. */ public class GodotApp extends FullScreenGodotApp { + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(R.style.GodotAppMainTheme); + super.onCreate(savedInstanceState); + } } diff --git a/platform/android/java/lib/res/values/dimens.xml b/platform/android/java/lib/res/values/dimens.xml new file mode 100644 index 0000000000..9034dbbcc1 --- /dev/null +++ b/platform/android/java/lib/res/values/dimens.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="text_edit_height">48dp</dimen> +</resources> diff --git a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java index 138c2de94c..5aa48d87da 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java +++ b/platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java @@ -34,6 +34,8 @@ import android.content.Intent; import android.os.Bundle; import android.view.KeyEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; /** @@ -43,13 +45,18 @@ import androidx.fragment.app.FragmentActivity; * within an Android app. */ public abstract class FullScreenGodotApp extends FragmentActivity { - protected Godot godotFragment; + @Nullable + private Godot godotFragment; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.godot_app_layout); - godotFragment = new Godot(); + godotFragment = initGodotInstance(); + if (godotFragment == null) { + throw new IllegalStateException("Godot instance must be non-null."); + } + getSupportFragmentManager().beginTransaction().replace(R.id.godot_fragment_container, godotFragment).setPrimaryNavigationFragment(godotFragment).commitNowAllowingStateLoss(); } @@ -76,4 +83,17 @@ public abstract class FullScreenGodotApp extends FragmentActivity { } return super.onKeyMultiple(inKeyCode, repeatCount, event); } + + /** + * Used to initialize the Godot fragment instance in {@link FullScreenGodotApp#onCreate(Bundle)}. + */ + @NonNull + protected Godot initGodotInstance() { + return new Godot(); + } + + @Nullable + protected final Godot getGodotFragment() { + return godotFragment; + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index 1ae400abb5..524f32bf5e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -151,8 +151,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private void setButtonPausedState(boolean paused) { mStatePaused = paused; - int stringResourceID = paused ? R.string.text_button_resume : - R.string.text_button_pause; + int stringResourceID = paused ? R.string.text_button_resume : R.string.text_button_pause; mPauseButton.setText(stringResourceID); } @@ -221,7 +220,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC // GodotEditText layout GodotEditText editText = new GodotEditText(activity); - editText.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + editText.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, + (int)getResources().getDimension(R.dimen.text_edit_height))); // ...add to FrameLayout containerLayout.addView(editText); @@ -467,7 +467,6 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); Window window = activity.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE); pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index 4dd228e53b..c2f3c88416 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -461,9 +461,9 @@ public class GodotIO { return (int)(metrics.density * 160f); } - public void showKeyboard(String p_existing_text, int p_max_input_length, int p_cursor_start, int p_cursor_end) { + public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (edit != null) - edit.showKeyboard(p_existing_text, p_max_input_length, p_cursor_start, p_cursor_end); + edit.showKeyboard(p_existing_text, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); //InputMethodManager inputMgr = (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE); //inputMgr.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java index c0defd008e..c95339c583 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java @@ -36,6 +36,7 @@ import android.content.Context; import android.os.Handler; import android.os.Message; import android.text.InputFilter; +import android.text.InputType; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; @@ -58,7 +59,8 @@ public class GodotEditText extends EditText { private GodotTextInputWrapper mInputWrapper; private EditHandler sHandler = new EditHandler(this); private String mOriginText; - private int mMaxInputLength; + private int mMaxInputLength = Integer.MAX_VALUE; + private boolean mMultiline = false; private static class EditHandler extends Handler { private final WeakReference<GodotEditText> mEdit; @@ -95,7 +97,11 @@ public class GodotEditText extends EditText { protected void initView() { setPadding(0, 0, 0, 0); - setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); + setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_DONE); + } + + public boolean isMultiline() { + return mMultiline; } private void handleMessage(final Message msg) { @@ -115,6 +121,12 @@ public class GodotEditText extends EditText { edit.mInputWrapper.setSelection(false); } + int inputType = InputType.TYPE_CLASS_TEXT; + if (edit.isMultiline()) { + inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE; + } + edit.setInputType(inputType); + edit.mInputWrapper.setOriginText(text); edit.addTextChangedListener(edit.mInputWrapper); final InputMethodManager imm = (InputMethodManager)mRenderView.getView().getContext().getSystemService(Context.INPUT_METHOD_SERVICE); @@ -189,7 +201,7 @@ public class GodotEditText extends EditText { // =========================================================== // Methods // =========================================================== - public void showKeyboard(String p_existing_text, int p_max_input_length, int p_cursor_start, int p_cursor_end) { + public void showKeyboard(String p_existing_text, boolean p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { int maxInputLength = (p_max_input_length <= 0) ? Integer.MAX_VALUE : p_max_input_length; if (p_cursor_start == -1) { // cursor position not given this.mOriginText = p_existing_text; @@ -202,6 +214,8 @@ public class GodotEditText extends EditText { this.mMaxInputLength = maxInputLength - (p_existing_text.length() - p_cursor_end); } + this.mMultiline = p_multiline; + final Message msg = new Message(); msg.what = HANDLER_OPEN_IME_KEYBOARD; msg.obj = this; diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index 9c7cf9f341..4dd1054738 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -123,7 +123,7 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene public void run() { for (int i = 0; i < count; ++i) { int key = newChars[i]; - if (key == '\n') { + if ((key == '\n') && !mEdit.isMultiline()) { // Return keys are handled through action events continue; } @@ -151,7 +151,7 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene }); } - if (pActionID == EditorInfo.IME_NULL) { + if (pActionID == EditorInfo.IME_ACTION_DONE) { // Enter key has been pressed GodotLib.key(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER, 0, true); GodotLib.key(KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER, 0, false); diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt index aeb4628d5d..7fa8e3b4e5 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt @@ -71,7 +71,7 @@ internal class VkRenderer { */ fun onVkSurfaceChanged(surface: Surface, width: Int, height: Int) { GodotLib.resize(surface, width, height) - + for (plugin in pluginRegistry.getAllPlugins()) { plugin.onVkSurfaceChanged(surface, width, height) } diff --git a/platform/android/java_godot_io_wrapper.cpp b/platform/android/java_godot_io_wrapper.cpp index 0a42adeaf2..4ccbc6b97e 100644 --- a/platform/android/java_godot_io_wrapper.cpp +++ b/platform/android/java_godot_io_wrapper.cpp @@ -53,7 +53,7 @@ GodotIOJavaWrapper::GodotIOJavaWrapper(JNIEnv *p_env, jobject p_godot_io_instanc _get_model = p_env->GetMethodID(cls, "getModel", "()Ljava/lang/String;"); _get_screen_DPI = p_env->GetMethodID(cls, "getScreenDPI", "()I"); _get_unique_id = p_env->GetMethodID(cls, "getUniqueID", "()Ljava/lang/String;"); - _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;III)V"); + _show_keyboard = p_env->GetMethodID(cls, "showKeyboard", "(Ljava/lang/String;ZIII)V"); _hide_keyboard = p_env->GetMethodID(cls, "hideKeyboard", "()V"); _set_screen_orientation = p_env->GetMethodID(cls, "setScreenOrientation", "(I)V"); _get_screen_orientation = p_env->GetMethodID(cls, "getScreenOrientation", "()I"); @@ -132,11 +132,11 @@ bool GodotIOJavaWrapper::has_vk() { return (_show_keyboard != 0) && (_hide_keyboard != 0); } -void GodotIOJavaWrapper::show_vk(const String &p_existing, int p_max_input_length, int p_cursor_start, int p_cursor_end) { +void GodotIOJavaWrapper::show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { if (_show_keyboard) { JNIEnv *env = ThreadAndroid::get_env(); jstring jStr = env->NewStringUTF(p_existing.utf8().get_data()); - env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_max_input_length, p_cursor_start, p_cursor_end); + env->CallVoidMethod(godot_io_instance, _show_keyboard, jStr, p_multiline, p_max_input_length, p_cursor_start, p_cursor_end); } } diff --git a/platform/android/java_godot_io_wrapper.h b/platform/android/java_godot_io_wrapper.h index 1742021379..6465ded985 100644 --- a/platform/android/java_godot_io_wrapper.h +++ b/platform/android/java_godot_io_wrapper.h @@ -70,7 +70,7 @@ public: int get_screen_dpi(); String get_unique_id(); bool has_vk(); - void show_vk(const String &p_existing, int p_max_input_length, int p_cursor_start, int p_cursor_end); + void show_vk(const String &p_existing, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end); void hide_vk(); int get_vk_height(); void set_vk_height(int p_height); diff --git a/platform/android/logo.png b/platform/android/logo.png Binary files differindex df445f6a9c..f44d360a25 100644 --- a/platform/android/logo.png +++ b/platform/android/logo.png diff --git a/platform/iphone/SCsub b/platform/iphone/SCsub index b72d29149c..1dd37dabe5 100644 --- a/platform/iphone/SCsub +++ b/platform/iphone/SCsub @@ -3,10 +3,8 @@ Import("env") iphone_lib = [ - "godot_iphone.cpp", - "os_iphone.cpp", - "semaphore_iphone.cpp", - "gl_view.mm", + "godot_iphone.mm", + "os_iphone.mm", "main.m", "app_delegate.mm", "view_controller.mm", @@ -15,6 +13,14 @@ iphone_lib = [ "icloud.mm", "ios.mm", "vulkan_context_iphone.mm", + "display_server_iphone.mm", + "joypad_iphone.mm", + "godot_view.mm", + "display_layer.mm", + "godot_view_renderer.mm", + "godot_view_gesture_recognizer.mm", + "device_metrics.m", + "native_video_view.m", ] env_ios = env.Clone() diff --git a/platform/iphone/app_delegate.h b/platform/iphone/app_delegate.h index 27552d781a..2f082f1e07 100644 --- a/platform/iphone/app_delegate.h +++ b/platform/iphone/app_delegate.h @@ -28,29 +28,20 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#if defined(OPENGL_ENABLED) -#import "gl_view.h" -#endif -#import "view_controller.h" #import <UIKit/UIKit.h> -#import <CoreMotion/CoreMotion.h> +@class ViewController; // FIXME: Add support for both GLES2 and Vulkan when GLES2 is implemented again, // so it can't be done with compilation time branching. //#if defined(OPENGL_ENABLED) //@interface AppDelegate : NSObject <UIApplicationDelegate, GLViewDelegate> { //#endif -#if defined(VULKAN_ENABLED) -@interface AppDelegate : NSObject <UIApplicationDelegate> { -#endif - //@property (strong, nonatomic) UIWindow *window; - ViewController *view_controller; - bool is_focus_out; -}; +//#if defined(VULKAN_ENABLED) +@interface AppDelegate : NSObject <UIApplicationDelegate> +//#endif @property(strong, nonatomic) UIWindow *window; - -+ (ViewController *)getViewController; +@property(strong, class, readonly, nonatomic) ViewController *viewController; @end diff --git a/platform/iphone/app_delegate.mm b/platform/iphone/app_delegate.mm index c4ef185bf1..40a63d7ad2 100644 --- a/platform/iphone/app_delegate.mm +++ b/platform/iphone/app_delegate.mm @@ -29,644 +29,60 @@ /*************************************************************************/ #import "app_delegate.h" - #include "core/project_settings.h" #include "drivers/coreaudio/audio_driver_coreaudio.h" -#if defined(OPENGL_ENABLED) -#import "gl_view.h" -#endif +#import "godot_view.h" #include "main/main.h" #include "os_iphone.h" +#import "view_controller.h" -#import "GameController/GameController.h" #import <AudioToolbox/AudioServices.h> -#define kFilteringFactor 0.1 #define kRenderingFrequency 60 -#define kAccelerometerFrequency 100.0 // Hz - -Error _shell_open(String); -void _set_keep_screen_on(bool p_enabled); - -Error _shell_open(String p_uri) { - NSString *url = [[NSString alloc] initWithUTF8String:p_uri.utf8().get_data()]; - - if (![[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:url]]) - return ERR_CANT_OPEN; - - printf("opening url %ls\n", p_uri.c_str()); - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]]; - [url release]; - return OK; -}; - -void _set_keep_screen_on(bool p_enabled) { - [[UIApplication sharedApplication] setIdleTimerDisabled:(BOOL)p_enabled]; -}; - -void _vibrate() { - AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); -}; - -@implementation AppDelegate - -@synthesize window; extern int gargc; extern char **gargv; -extern int iphone_main(int, int, int, char **, String); + +extern int iphone_main(int, char **, String); extern void iphone_finish(); -CMMotionManager *motionManager; -bool motionInitialised; +@implementation AppDelegate static ViewController *mainViewController = nil; -+ (ViewController *)getViewController { - return mainViewController; -} - -NSMutableDictionary *ios_joysticks = nil; -NSMutableArray *pending_ios_joysticks = nil; - -- (GCControllerPlayerIndex)getFreePlayerIndex { - bool have_player_1 = false; - bool have_player_2 = false; - bool have_player_3 = false; - bool have_player_4 = false; - - if (ios_joysticks == nil) { - NSArray *keys = [ios_joysticks allKeys]; - for (NSNumber *key in keys) { - GCController *controller = [ios_joysticks objectForKey:key]; - if (controller.playerIndex == GCControllerPlayerIndex1) { - have_player_1 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex2) { - have_player_2 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex3) { - have_player_3 = true; - } else if (controller.playerIndex == GCControllerPlayerIndex4) { - have_player_4 = true; - }; - }; - }; - - if (!have_player_1) { - return GCControllerPlayerIndex1; - } else if (!have_player_2) { - return GCControllerPlayerIndex2; - } else if (!have_player_3) { - return GCControllerPlayerIndex3; - } else if (!have_player_4) { - return GCControllerPlayerIndex4; - } else { - return GCControllerPlayerIndexUnset; - }; -}; - -void _ios_add_joystick(GCController *controller, AppDelegate *delegate) { - // get a new id for our controller - int joy_id = OSIPhone::get_singleton()->get_unused_joy_id(); - if (joy_id != -1) { - // assign our player index - if (controller.playerIndex == GCControllerPlayerIndexUnset) { - controller.playerIndex = [delegate getFreePlayerIndex]; - }; - - // tell Godot about our new controller - OSIPhone::get_singleton()->joy_connection_changed( - joy_id, true, [controller.vendorName UTF8String]); - - // add it to our dictionary, this will retain our controllers - [ios_joysticks setObject:controller - forKey:[NSNumber numberWithInt:joy_id]]; - - // set our input handler - [delegate setControllerInputHandler:controller]; - } else { - printf("Couldn't retrieve new joy id\n"); - }; -} - -static void on_focus_out(ViewController *view_controller, bool *is_focus_out) { - if (!*is_focus_out) { - *is_focus_out = true; - if (OS::get_singleton()->get_main_loop()) - OS::get_singleton()->get_main_loop()->notification( - MainLoop::NOTIFICATION_WM_FOCUS_OUT); - - [view_controller.view stopAnimation]; - if (OS::get_singleton()->native_video_is_playing()) { - OSIPhone::get_singleton()->native_video_focus_out(); - } - AudioDriverCoreAudio *audio = dynamic_cast<AudioDriverCoreAudio *>(AudioDriverCoreAudio::get_singleton()); - if (audio) - audio->stop(); - } -} - -static void on_focus_in(ViewController *view_controller, bool *is_focus_out) { - if (*is_focus_out) { - *is_focus_out = false; - if (OS::get_singleton()->get_main_loop()) - OS::get_singleton()->get_main_loop()->notification( - MainLoop::NOTIFICATION_WM_FOCUS_IN); - - [view_controller.view startAnimation]; - if (OSIPhone::get_singleton()->native_video_is_playing()) { - OSIPhone::get_singleton()->native_video_unpause(); - } - - AudioDriverCoreAudio *audio = dynamic_cast<AudioDriverCoreAudio *>(AudioDriverCoreAudio::get_singleton()); - if (audio) - audio->start(); - } ++ (ViewController *)viewController { + return mainViewController; } -- (void)controllerWasConnected:(NSNotification *)notification { - // create our dictionary if we don't have one yet - if (ios_joysticks == nil) { - ios_joysticks = [[NSMutableDictionary alloc] init]; - }; - - // get our controller - GCController *controller = (GCController *)notification.object; - if (controller == nil) { - printf("Couldn't retrieve new controller\n"); - } else if ([[ios_joysticks allKeysForObject:controller] count] != 0) { - printf("Controller is already registered\n"); - } else if (frame_count > 1) { - _ios_add_joystick(controller, self); - } else { - if (pending_ios_joysticks == nil) - pending_ios_joysticks = [[NSMutableArray alloc] init]; - [pending_ios_joysticks addObject:controller]; - }; -}; - -- (void)controllerWasDisconnected:(NSNotification *)notification { - if (ios_joysticks != nil) { - // find our joystick, there should be only one in our dictionary - GCController *controller = (GCController *)notification.object; - NSArray *keys = [ios_joysticks allKeysForObject:controller]; - for (NSNumber *key in keys) { - // tell Godot this joystick is no longer there - int joy_id = [key intValue]; - OSIPhone::get_singleton()->joy_connection_changed(joy_id, false, ""); - - // and remove it from our dictionary - [ios_joysticks removeObjectForKey:key]; - }; - }; -}; - -- (int)getJoyIdForController:(GCController *)controller { - if (ios_joysticks != nil) { - // find our joystick, there should be only one in our dictionary - NSArray *keys = [ios_joysticks allKeysForObject:controller]; - for (NSNumber *key in keys) { - int joy_id = [key intValue]; - return joy_id; - }; - }; - - return -1; -}; - -- (void)setControllerInputHandler:(GCController *)controller { - // Hook in the callback handler for the correct gamepad profile. - // This is a bit of a weird design choice on Apples part. - // You need to select the most capable gamepad profile for the - // gamepad attached. - if (controller.extendedGamepad != nil) { - // The extended gamepad profile has all the input you could possibly find on - // a gamepad but will only be active if your gamepad actually has all of - // these... - controller.extendedGamepad.valueChangedHandler = ^( - GCExtendedGamepad *gamepad, GCControllerElement *element) { - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonB) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_B, - gamepad.buttonB.isPressed); - } else if (element == gamepad.buttonX) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.buttonY) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_Y, - gamepad.buttonY.isPressed); - } else if (element == gamepad.leftShoulder) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_LEFT_SHOULDER, - gamepad.leftShoulder.isPressed); - } else if (element == gamepad.rightShoulder) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_RIGHT_SHOULDER, - gamepad.rightShoulder.isPressed); - } else if (element == gamepad.dpad) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, - gamepad.dpad.up.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, - gamepad.dpad.down.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, - gamepad.dpad.left.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, - gamepad.dpad.right.isPressed); - }; - - InputDefault::JoyAxis jx; - jx.min = -1; - if (element == gamepad.leftThumbstick) { - jx.value = gamepad.leftThumbstick.xAxis.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_LEFT_X, jx); - jx.value = -gamepad.leftThumbstick.yAxis.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_LEFT_Y, jx); - } else if (element == gamepad.rightThumbstick) { - jx.value = gamepad.rightThumbstick.xAxis.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_RIGHT_X, jx); - jx.value = -gamepad.rightThumbstick.yAxis.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_RIGHT_Y, jx); - } else if (element == gamepad.leftTrigger) { - jx.value = gamepad.leftTrigger.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_LEFT, jx); - } else if (element == gamepad.rightTrigger) { - jx.value = gamepad.rightTrigger.value; - OSIPhone::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_RIGHT, jx); - }; - }; - } else if (controller.gamepad != nil) { - // gamepad is the standard profile with 4 buttons, shoulder buttons and a - // D-pad - controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, - GCControllerElement *element) { - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonB) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_B, - gamepad.buttonB.isPressed); - } else if (element == gamepad.buttonX) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.buttonY) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_Y, - gamepad.buttonY.isPressed); - } else if (element == gamepad.leftShoulder) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_LEFT_SHOULDER, - gamepad.leftShoulder.isPressed); - } else if (element == gamepad.rightShoulder) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_RIGHT_SHOULDER, - gamepad.rightShoulder.isPressed); - } else if (element == gamepad.dpad) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, - gamepad.dpad.up.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, - gamepad.dpad.down.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, - gamepad.dpad.left.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, - gamepad.dpad.right.isPressed); - }; - }; -#ifdef ADD_MICRO_GAMEPAD // disabling this for now, only available on iOS 9+, - // while we are setting that as the minimum, seems our - // build environment doesn't like it - } else if (controller.microGamepad != nil) { - // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad - controller.microGamepad.valueChangedHandler = - ^(GCMicroGamepad *gamepad, GCControllerElement *element) { - int joy_id = [self getJoyIdForController:controller]; - - if (element == gamepad.buttonA) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, - gamepad.buttonA.isPressed); - } else if (element == gamepad.buttonX) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, - gamepad.buttonX.isPressed); - } else if (element == gamepad.dpad) { - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, - gamepad.dpad.up.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, - gamepad.dpad.down.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, - gamepad.dpad.left.isPressed); - OSIPhone::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, - gamepad.dpad.right.isPressed); - }; - }; -#endif - }; - - ///@TODO need to add support for controller.motion which gives us access to - /// the orientation of the device (if supported) - - ///@TODO need to add support for controllerPausedHandler which should be a - /// toggle -}; - -- (void)initGameControllers { - // get told when controllers connect, this will be called right away for - // already connected controllers - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasConnected:) - name:GCControllerDidConnectNotification - object:nil]; - - // get told when controllers disconnect - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(controllerWasDisconnected:) - name:GCControllerDidDisconnectNotification - object:nil]; -}; - -- (void)deinitGameControllers { - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:GCControllerDidConnectNotification - object:nil]; - [[NSNotificationCenter defaultCenter] - removeObserver:self - name:GCControllerDidDisconnectNotification - object:nil]; - - if (ios_joysticks != nil) { - [ios_joysticks dealloc]; - ios_joysticks = nil; - }; - - if (pending_ios_joysticks != nil) { - [pending_ios_joysticks dealloc]; - pending_ios_joysticks = nil; - }; -}; - -OS::VideoMode _get_video_mode() { - int backingWidth; - int backingHeight; -#if defined(OPENGL_ENABLED) - glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, - GL_RENDERBUFFER_WIDTH_OES, &backingWidth); - glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, - GL_RENDERBUFFER_HEIGHT_OES, &backingHeight); -#endif - - OS::VideoMode vm; - vm.fullscreen = true; - vm.width = backingWidth; - vm.height = backingHeight; - vm.resizable = false; - return vm; -}; - -static int frame_count = 0; -- (void)drawView:(UIView *)view; -{ - switch (frame_count) { - case 0: { - OS::get_singleton()->set_video_mode(_get_video_mode()); - - if (!OS::get_singleton()) { - exit(0); - }; - ++frame_count; - - NSString *locale_code = [[NSLocale currentLocale] localeIdentifier]; - OSIPhone::get_singleton()->set_locale( - String::utf8([locale_code UTF8String])); - - NSString *uuid; - if ([[UIDevice currentDevice] - respondsToSelector:@selector(identifierForVendor)]) { - uuid = [UIDevice currentDevice].identifierForVendor.UUIDString; - } else { - // before iOS 6, so just generate an identifier and store it - uuid = [[NSUserDefaults standardUserDefaults] - objectForKey:@"identiferForVendor"]; - if (!uuid) { - CFUUIDRef cfuuid = CFUUIDCreate(NULL); - uuid = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, cfuuid); - CFRelease(cfuuid); - [[NSUserDefaults standardUserDefaults] - setObject:uuid - forKey:@"identifierForVendor"]; - } - } - - OSIPhone::get_singleton()->set_unique_id(String::utf8([uuid UTF8String])); - - }; break; - - case 1: { - Main::setup2(); - ++frame_count; - - if (pending_ios_joysticks != nil) { - for (GCController *controller in pending_ios_joysticks) { - _ios_add_joystick(controller, self); - } - [pending_ios_joysticks dealloc]; - pending_ios_joysticks = nil; - } - - // this might be necessary before here - NSDictionary *dict = [[NSBundle mainBundle] infoDictionary]; - for (NSString *key in dict) { - NSObject *value = [dict objectForKey:key]; - String ukey = String::utf8([key UTF8String]); - - // we need a NSObject to Variant conversor - - if ([value isKindOfClass:[NSString class]]) { - NSString *str = (NSString *)value; - String uval = String::utf8([str UTF8String]); - - ProjectSettings::get_singleton()->set("Info.plist/" + ukey, uval); - - } else if ([value isKindOfClass:[NSNumber class]]) { - NSNumber *n = (NSNumber *)value; - double dval = [n doubleValue]; - - ProjectSettings::get_singleton()->set("Info.plist/" + ukey, dval); - }; - // do stuff - } - - }; break; - - case 2: { - Main::start(); - ++frame_count; - - }; break; // no fallthrough - - default: { - if (OSIPhone::get_singleton()) { - // OSIPhone::get_singleton()->update_accelerometer(accel[0], accel[1], - // accel[2]); - if (motionInitialised) { - // Just using polling approach for now, we can set this up so it sends - // data to us in intervals, might be better. See Apple reference pages - // for more details: - // https://developer.apple.com/reference/coremotion/cmmotionmanager?language=objc - - // Apple splits our accelerometer date into a gravity and user movement - // component. We add them back together - CMAcceleration gravity = motionManager.deviceMotion.gravity; - CMAcceleration acceleration = - motionManager.deviceMotion.userAcceleration; - - ///@TODO We don't seem to be getting data here, is my device broken or - /// is this code incorrect? - CMMagneticField magnetic = - motionManager.deviceMotion.magneticField.field; - - ///@TODO we can access rotationRate as a CMRotationRate variable - ///(processed date) or CMGyroData (raw data), have to see what works - /// best - CMRotationRate rotation = motionManager.deviceMotion.rotationRate; - - // Adjust for screen orientation. - // [[UIDevice currentDevice] orientation] changes even if we've fixed - // our orientation which is not a good thing when you're trying to get - // your user to move the screen in all directions and want consistent - // output - - ///@TODO Using [[UIApplication sharedApplication] statusBarOrientation] - /// is a bit of a hack. Godot obviously knows the orientation so maybe - /// we - // can use that instead? (note that left and right seem swapped) - - switch ([[UIApplication sharedApplication] statusBarOrientation]) { - case UIDeviceOrientationLandscapeLeft: { - OSIPhone::get_singleton()->update_gravity(-gravity.y, gravity.x, - gravity.z); - OSIPhone::get_singleton()->update_accelerometer( - -(acceleration.y + gravity.y), (acceleration.x + gravity.x), - acceleration.z + gravity.z); - OSIPhone::get_singleton()->update_magnetometer( - -magnetic.y, magnetic.x, magnetic.z); - OSIPhone::get_singleton()->update_gyroscope(-rotation.y, rotation.x, - rotation.z); - }; break; - case UIDeviceOrientationLandscapeRight: { - OSIPhone::get_singleton()->update_gravity(gravity.y, -gravity.x, - gravity.z); - OSIPhone::get_singleton()->update_accelerometer( - (acceleration.y + gravity.y), -(acceleration.x + gravity.x), - acceleration.z + gravity.z); - OSIPhone::get_singleton()->update_magnetometer( - magnetic.y, -magnetic.x, magnetic.z); - OSIPhone::get_singleton()->update_gyroscope(rotation.y, -rotation.x, - rotation.z); - }; break; - case UIDeviceOrientationPortraitUpsideDown: { - OSIPhone::get_singleton()->update_gravity(-gravity.x, gravity.y, - gravity.z); - OSIPhone::get_singleton()->update_accelerometer( - -(acceleration.x + gravity.x), (acceleration.y + gravity.y), - acceleration.z + gravity.z); - OSIPhone::get_singleton()->update_magnetometer( - -magnetic.x, magnetic.y, magnetic.z); - OSIPhone::get_singleton()->update_gyroscope(-rotation.x, rotation.y, - rotation.z); - }; break; - default: { // assume portrait - OSIPhone::get_singleton()->update_gravity(gravity.x, gravity.y, - gravity.z); - OSIPhone::get_singleton()->update_accelerometer( - acceleration.x + gravity.x, acceleration.y + gravity.y, - acceleration.z + gravity.z); - OSIPhone::get_singleton()->update_magnetometer(magnetic.x, magnetic.y, - magnetic.z); - OSIPhone::get_singleton()->update_gyroscope(rotation.x, rotation.y, - rotation.z); - }; break; - }; - } - - bool quit_request = OSIPhone::get_singleton()->iterate(); - }; - - }; break; - }; -}; - -- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { - if (OS::get_singleton()->get_main_loop()) { - OS::get_singleton()->get_main_loop()->notification( - MainLoop::NOTIFICATION_OS_MEMORY_WARNING); - } -}; - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - CGRect rect = [[UIScreen mainScreen] bounds]; + // TODO: might be required to make an early return, so app wouldn't crash because of timeout. + // TODO: logo screen is not displayed while shaders are compiling + // DummyViewController(Splash/LoadingViewController) -> setup -> GodotViewController - is_focus_out = false; - - [application setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone]; - // disable idle timer - // application.idleTimerDisabled = YES; + CGRect windowBounds = [[UIScreen mainScreen] bounds]; // Create a full-screen window - window = [[UIWindow alloc] initWithFrame:rect]; - - OS::VideoMode vm = _get_video_mode(); + self.window = [[UIWindow alloc] initWithFrame:windowBounds]; - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, - NSUserDomainMask, YES); + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; - int err = iphone_main(vm.width, vm.height, gargc, gargv, String::utf8([documentsDirectory UTF8String])); + int err = iphone_main(gargc, gargv, String::utf8([documentsDirectory UTF8String])); + if (err != 0) { // bail, things did not go very well for us, should probably output a message on screen with our error code... exit(0); - return FALSE; + return NO; }; -#if defined(OPENGL_ENABLED) - // WARNING: We must *always* create the GLView after we have constructed the - // OS with iphone_main. This allows the GLView to access project settings so - // it can properly initialize the OpenGL context - GLView *glView = [[GLView alloc] initWithFrame:rect]; - glView.delegate = self; - - view_controller = [[ViewController alloc] init]; - view_controller.view = glView; - - _set_keep_screen_on(bool(GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true)) ? YES : NO); - glView.useCADisplayLink = - bool(GLOBAL_DEF("display.iOS/use_cadisplaylink", true)) ? YES : NO; - printf("cadisaplylink: %d", glView.useCADisplayLink); - glView.animationInterval = 1.0 / kRenderingFrequency; - [glView startAnimation]; -#endif - -#if defined(VULKAN_ENABLED) - view_controller = [[ViewController alloc] init]; -#endif + ViewController *viewController = [[ViewController alloc] init]; + viewController.godotView.useCADisplayLink = bool(GLOBAL_DEF("display.iOS/use_cadisplaylink", true)) ? YES : NO; + viewController.godotView.renderingInterval = 1.0 / kRenderingFrequency; - window.rootViewController = view_controller; + self.window.rootViewController = viewController; // Show the window - [window makeKeyAndVisible]; - - // Configure and start accelerometer - if (!motionInitialised) { - motionManager = [[CMMotionManager alloc] init]; - if (motionManager.deviceMotionAvailable) { - motionManager.deviceMotionUpdateInterval = 1.0 / 70.0; - [motionManager startDeviceMotionUpdatesUsingReferenceFrame: - CMAttitudeReferenceFrameXMagneticNorthZVertical]; - motionInitialised = YES; - }; - }; - - [self initGameControllers]; + [self.window makeKeyAndVisible]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -674,40 +90,33 @@ static int frame_count = 0; name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; - // OSIPhone::screen_width = rect.size.width - rect.origin.x; - // OSIPhone::screen_height = rect.size.height - rect.origin.y; - - mainViewController = view_controller; + mainViewController = viewController; // prevent to stop music in another background app [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil]; - return TRUE; + return YES; }; - (void)onAudioInterruption:(NSNotification *)notification { if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) { if ([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeBegan]]) { NSLog(@"Audio interruption began"); - on_focus_out(view_controller, &is_focus_out); + OSIPhone::get_singleton()->on_focus_out(); } else if ([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded]]) { NSLog(@"Audio interruption ended"); - on_focus_in(view_controller, &is_focus_out); + OSIPhone::get_singleton()->on_focus_in(); } } }; -- (void)applicationWillTerminate:(UIApplication *)application { - [self deinitGameControllers]; - - if (motionInitialised) { - ///@TODO is this the right place to clean this up? - [motionManager stopDeviceMotionUpdates]; - [motionManager release]; - motionManager = nil; - motionInitialised = NO; - }; +- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_OS_MEMORY_WARNING); + } +}; +- (void)applicationWillTerminate:(UIApplication *)application { iphone_finish(); }; @@ -722,16 +131,15 @@ static int frame_count = 0; // notification panel by swiping from the upper part of the screen. - (void)applicationWillResignActive:(UIApplication *)application { - on_focus_out(view_controller, &is_focus_out); + OSIPhone::get_singleton()->on_focus_out(); } - (void)applicationDidBecomeActive:(UIApplication *)application { - on_focus_in(view_controller, &is_focus_out); + OSIPhone::get_singleton()->on_focus_in(); } - (void)dealloc { - [window release]; - [super dealloc]; + self.window = nil; } @end diff --git a/platform/iphone/detect.py b/platform/iphone/detect.py index c48eb7ff0e..5ebabdd3dc 100644 --- a/platform/iphone/detect.py +++ b/platform/iphone/detect.py @@ -31,7 +31,8 @@ def get_opts(): ("IPHONESDK", "Path to the iPhone SDK", ""), BoolVariable( "use_static_mvk", - "Link MoltenVK statically as Level-0 driver (better portability) or use Vulkan ICD loader (enables validation layers)", + "Link MoltenVK statically as Level-0 driver (better portability) or use Vulkan ICD loader (enables" + " validation layers)", False, ), BoolVariable("game_center", "Support for game center", True), @@ -120,18 +121,31 @@ def configure(env): CCFLAGS=( "-arch " + arch_flag - + " -fobjc-abi-version=2 -fobjc-legacy-dispatch -fmessage-length=0 -fpascal-strings -fblocks -fasm-blocks -isysroot $IPHONESDK -mios-simulator-version-min=10.0" + + " -fobjc-abi-version=2 -fobjc-legacy-dispatch -fmessage-length=0 -fpascal-strings -fblocks" + " -fasm-blocks -isysroot $IPHONESDK -mios-simulator-version-min=13.0" ).split() ) elif env["arch"] == "arm": detect_darwin_sdk_path("iphone", env) env.Append( - CCFLAGS='-fno-objc-arc -arch armv7 -fmessage-length=0 -fno-strict-aliasing -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits -fpascal-strings -fblocks -isysroot $IPHONESDK -fvisibility=hidden -mthumb "-DIBOutlet=__attribute__((iboutlet))" "-DIBOutletCollection(ClassName)=__attribute__((iboutletcollection(ClassName)))" "-DIBAction=void)__attribute__((ibaction)" -miphoneos-version-min=10.0 -MMD -MT dependencies'.split() + CCFLAGS=( + "-fobjc-arc -arch armv7 -fmessage-length=0 -fno-strict-aliasing" + " -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits" + " -fpascal-strings -fblocks -isysroot $IPHONESDK -fvisibility=hidden -mthumb" + ' "-DIBOutlet=__attribute__((iboutlet))"' + ' "-DIBOutletCollection(ClassName)=__attribute__((iboutletcollection(ClassName)))"' + ' "-DIBAction=void)__attribute__((ibaction)" -miphoneos-version-min=11.0 -MMD -MT dependencies'.split() + ) ) elif env["arch"] == "arm64": detect_darwin_sdk_path("iphone", env) env.Append( - CCFLAGS="-fno-objc-arc -arch arm64 -fmessage-length=0 -fno-strict-aliasing -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits -fpascal-strings -fblocks -fvisibility=hidden -MMD -MT dependencies -miphoneos-version-min=10.0 -isysroot $IPHONESDK".split() + CCFLAGS=( + "-fobjc-arc -arch arm64 -fmessage-length=0 -fno-strict-aliasing" + " -fdiagnostics-print-source-range-info -fdiagnostics-show-category=id -fdiagnostics-parseable-fixits" + " -fpascal-strings -fblocks -fvisibility=hidden -MMD -MT dependencies -miphoneos-version-min=11.0" + " -isysroot $IPHONESDK".split() + ) ) env.Append(CPPDEFINES=["NEED_LONG_INT"]) env.Append(CPPDEFINES=["LIBYUV_DISABLE_NEON"]) @@ -143,6 +157,9 @@ def configure(env): else: env.Append(CCFLAGS=["-fno-exceptions"]) + # Temp fix for ABS/MAX/MIN macros in iPhone SDK blocking compilation + env.Append(CCFLAGS=["-Wno-ambiguous-macro"]) + ## Link flags if env["arch"] == "x86" or env["arch"] == "x86_64": @@ -151,7 +168,7 @@ def configure(env): LINKFLAGS=[ "-arch", arch_flag, - "-mios-simulator-version-min=10.0", + "-mios-simulator-version-min=13.0", "-isysroot", "$IPHONESDK", "-Xlinker", @@ -162,9 +179,9 @@ def configure(env): ] ) elif env["arch"] == "arm": - env.Append(LINKFLAGS=["-arch", "armv7", "-Wl,-dead_strip", "-miphoneos-version-min=10.0"]) + env.Append(LINKFLAGS=["-arch", "armv7", "-Wl,-dead_strip", "-miphoneos-version-min=11.0"]) if env["arch"] == "arm64": - env.Append(LINKFLAGS=["-arch", "arm64", "-Wl,-dead_strip", "-miphoneos-version-min=10.0"]) + env.Append(LINKFLAGS=["-arch", "arm64", "-Wl,-dead_strip", "-miphoneos-version-min=11.0"]) env.Append( LINKFLAGS=[ @@ -218,7 +235,10 @@ def configure(env): env.Append(CPPDEFINES=["ICLOUD_ENABLED"]) env.Prepend( - CPPPATH=["$IPHONESDK/usr/include", "$IPHONESDK/System/Library/Frameworks/AudioUnit.framework/Headers",] + CPPPATH=[ + "$IPHONESDK/usr/include", + "$IPHONESDK/System/Library/Frameworks/AudioUnit.framework/Headers", + ] ) env["ENV"]["CODESIGN_ALLOCATE"] = "/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/codesign_allocate" @@ -228,8 +248,7 @@ def configure(env): env.Append(CPPDEFINES=["VULKAN_ENABLED"]) env.Append(LINKFLAGS=["-framework", "IOSurface"]) - if env["use_static_mvk"]: - env.Append(LINKFLAGS=["-framework", "MoltenVK"]) - env["builtin_vulkan"] = False - elif not env["builtin_vulkan"]: - env.Append(LIBS=["vulkan"]) + + # Use Static Vulkan for iOS. Dynamic Framework works fine too. + env.Append(LINKFLAGS=["-framework", "MoltenVK"]) + env["builtin_vulkan"] = False diff --git a/platform/iphone/device_metrics.h b/platform/iphone/device_metrics.h new file mode 100644 index 0000000000..6d0ff49077 --- /dev/null +++ b/platform/iphone/device_metrics.h @@ -0,0 +1,37 @@ +/*************************************************************************/ +/* device_metrics.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 <Foundation/Foundation.h> + +@interface GodotDeviceMetrics : NSObject + +@property(nonatomic, class, readonly, strong) NSDictionary<NSArray *, NSNumber *> *dpiList; + +@end diff --git a/platform/iphone/device_metrics.m b/platform/iphone/device_metrics.m new file mode 100644 index 0000000000..747872bc49 --- /dev/null +++ b/platform/iphone/device_metrics.m @@ -0,0 +1,152 @@ +/*************************************************************************/ +/* device_metrics.m */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "device_metrics.h" + +@implementation GodotDeviceMetrics + ++ (NSDictionary *)dpiList { + return @{ + @[ + @"iPad1,1", + @"iPad2,1", + @"iPad2,2", + @"iPad2,3", + @"iPad2,4", + ] : @132, + @[ + @"iPhone1,1", + @"iPhone1,2", + @"iPhone2,1", + @"iPad2,5", + @"iPad2,6", + @"iPad2,7", + @"iPod1,1", + @"iPod2,1", + @"iPod3,1", + ] : @163, + @[ + @"iPad3,1", + @"iPad3,2", + @"iPad3,3", + @"iPad3,4", + @"iPad3,5", + @"iPad3,6", + @"iPad4,1", + @"iPad4,2", + @"iPad4,3", + @"iPad5,3", + @"iPad5,4", + @"iPad6,3", + @"iPad6,4", + @"iPad6,7", + @"iPad6,8", + @"iPad6,11", + @"iPad6,12", + @"iPad7,1", + @"iPad7,2", + @"iPad7,3", + @"iPad7,4", + @"iPad7,5", + @"iPad7,6", + @"iPad7,11", + @"iPad7,12", + @"iPad8,1", + @"iPad8,2", + @"iPad8,3", + @"iPad8,4", + @"iPad8,5", + @"iPad8,6", + @"iPad8,7", + @"iPad8,8", + @"iPad8,9", + @"iPad8,10", + @"iPad8,11", + @"iPad8,12", + @"iPad11,3", + @"iPad11,4", + ] : @264, + @[ + @"iPhone3,1", + @"iPhone3,2", + @"iPhone3,3", + @"iPhone4,1", + @"iPhone5,1", + @"iPhone5,2", + @"iPhone5,3", + @"iPhone5,4", + @"iPhone6,1", + @"iPhone6,2", + @"iPhone7,2", + @"iPhone8,1", + @"iPhone8,4", + @"iPhone9,1", + @"iPhone9,3", + @"iPhone10,1", + @"iPhone10,4", + @"iPhone11,8", + @"iPhone12,1", + @"iPhone12,8", + @"iPad4,4", + @"iPad4,5", + @"iPad4,6", + @"iPad4,7", + @"iPad4,8", + @"iPad4,9", + @"iPad5,1", + @"iPad5,2", + @"iPad11,1", + @"iPad11,2", + @"iPod4,1", + @"iPod5,1", + @"iPod7,1", + @"iPod9,1", + ] : @326, + @[ + @"iPhone7,1", + @"iPhone8,2", + @"iPhone9,2", + @"iPhone9,4", + @"iPhone10,2", + @"iPhone10,5", + ] : @401, + @[ + @"iPhone10,3", + @"iPhone10,6", + @"iPhone11,2", + @"iPhone11,4", + @"iPhone11,6", + @"iPhone12,3", + @"iPhone12,5", + ] : @458, + }; +} + +@end diff --git a/platform/iphone/display_layer.h b/platform/iphone/display_layer.h new file mode 100644 index 0000000000..bfde8f96a3 --- /dev/null +++ b/platform/iphone/display_layer.h @@ -0,0 +1,58 @@ +/*************************************************************************/ +/* display_layer.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 <OpenGLES/EAGLDrawable.h> +#import <QuartzCore/QuartzCore.h> + +@protocol DisplayLayer <NSObject> + +- (void)renderDisplayLayer; +- (void)initializeDisplayLayer; +- (void)layoutDisplayLayer; + +@end + +// An ugly workaround for iOS simulator +#if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR +#if defined(__IPHONE_13_0) +API_AVAILABLE(ios(13.0)) +@interface GodotMetalLayer : CAMetalLayer <DisplayLayer> +#else +@interface GodotMetalLayer : CALayer <DisplayLayer> +#endif +#else +@interface GodotMetalLayer : CAMetalLayer <DisplayLayer> +#endif +@end + +API_DEPRECATED("OpenGLES is deprecated", ios(2.0, 12.0)) +@interface GodotOpenGLLayer : CAEAGLLayer <DisplayLayer> + +@end diff --git a/platform/iphone/display_layer.mm b/platform/iphone/display_layer.mm new file mode 100644 index 0000000000..2716538b89 --- /dev/null +++ b/platform/iphone/display_layer.mm @@ -0,0 +1,183 @@ +/*************************************************************************/ +/* display_layer.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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_layer.h" +#include "core/os/keyboard.h" +#include "core/project_settings.h" +#include "display_server_iphone.h" +#include "main/main.h" +#include "os_iphone.h" +#include "servers/audio_server.h" + +#import <AudioToolbox/AudioServices.h> +#import <GameController/GameController.h> +#import <OpenGLES/EAGL.h> +#import <OpenGLES/ES1/gl.h> +#import <OpenGLES/ES1/glext.h> +#import <QuartzCore/QuartzCore.h> +#import <UIKit/UIKit.h> + +@implementation GodotMetalLayer + +- (void)initializeDisplayLayer { +#if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR + if (@available(iOS 13, *)) { + // Simulator supports Metal since iOS 13 + } else { + NSLog(@"iOS Simulator prior to iOS 13 does not support Metal rendering."); + } +#endif +} + +- (void)layoutDisplayLayer { +} + +- (void)renderDisplayLayer { +} + +@end + +@implementation GodotOpenGLLayer { + // The pixel dimensions of the backbuffer + GLint backingWidth; + GLint backingHeight; + + EAGLContext *context; + GLuint viewRenderbuffer, viewFramebuffer; + GLuint depthRenderbuffer; +} + +- (void)initializeDisplayLayer { + // Get our backing layer + + // Configure it so that it is opaque, does not retain the contents of the backbuffer when displayed, and uses RGBA8888 color. + self.opaque = YES; + self.drawableProperties = [NSDictionary + dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:FALSE], + kEAGLDrawablePropertyRetainedBacking, + kEAGLColorFormatRGBA8, + kEAGLDrawablePropertyColorFormat, + nil]; + + // FIXME: Add Vulkan support via MoltenVK. Add fallback code back? + + // Create GL ES 2 context + if (GLOBAL_GET("rendering/quality/driver/driver_name") == "GLES2") { + context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; + NSLog(@"Setting up an OpenGL ES 2.0 context."); + if (!context) { + NSLog(@"Failed to create OpenGL ES 2.0 context!"); + return; + } + } + + if (![EAGLContext setCurrentContext:context]) { + NSLog(@"Failed to set EAGLContext!"); + return; + } + if (![self createFramebuffer]) { + NSLog(@"Failed to create frame buffer!"); + return; + } +} + +- (void)layoutDisplayLayer { + [EAGLContext setCurrentContext:context]; + [self destroyFramebuffer]; + [self createFramebuffer]; +} + +- (void)renderDisplayLayer { + [EAGLContext setCurrentContext:context]; +} + +- (void)dealloc { + if ([EAGLContext currentContext] == context) { + [EAGLContext setCurrentContext:nil]; + } + + if (context) { + context = nil; + } +} + +- (BOOL)createFramebuffer { + glGenFramebuffersOES(1, &viewFramebuffer); + glGenRenderbuffersOES(1, &viewRenderbuffer); + + glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer); + glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer); + // This call associates the storage for the current render buffer with the EAGLDrawable (our CAself) + // allowing us to draw into a buffer that will later be rendered to screen wherever the layer is (which corresponds with our view). + [context renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:(id<EAGLDrawable>)self]; + glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, viewRenderbuffer); + + glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &backingWidth); + glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &backingHeight); + + // For this sample, we also need a depth buffer, so we'll create and attach one via another renderbuffer. + glGenRenderbuffersOES(1, &depthRenderbuffer); + glBindRenderbufferOES(GL_RENDERBUFFER_OES, depthRenderbuffer); + glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH_COMPONENT16_OES, backingWidth, backingHeight); + glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, depthRenderbuffer); + + if (glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) != GL_FRAMEBUFFER_COMPLETE_OES) { + NSLog(@"failed to make complete framebuffer object %x", glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES)); + return NO; + } + + // if (OS::get_singleton()) { + // OS::VideoMode vm; + // vm.fullscreen = true; + // vm.width = backingWidth; + // vm.height = backingHeight; + // vm.resizable = false; + // OS::get_singleton()->set_video_mode(vm); + // OSIPhone::get_singleton()->set_base_framebuffer(viewFramebuffer); + // }; + // gl_view_base_fb = viewFramebuffer; + + return YES; +} + +// Clean up any buffers we have allocated. +- (void)destroyFramebuffer { + glDeleteFramebuffersOES(1, &viewFramebuffer); + viewFramebuffer = 0; + glDeleteRenderbuffersOES(1, &viewRenderbuffer); + viewRenderbuffer = 0; + + if (depthRenderbuffer) { + glDeleteRenderbuffersOES(1, &depthRenderbuffer); + depthRenderbuffer = 0; + } +} + +@end diff --git a/platform/iphone/display_server_iphone.h b/platform/iphone/display_server_iphone.h new file mode 100644 index 0000000000..229b1e80db --- /dev/null +++ b/platform/iphone/display_server_iphone.h @@ -0,0 +1,202 @@ +/*************************************************************************/ +/* display_server_iphone.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#ifndef display_server_iphone_h +#define display_server_iphone_h + +#include "core/input/input.h" +#include "servers/display_server.h" + +#if defined(VULKAN_ENABLED) +#include "drivers/vulkan/rendering_device_vulkan.h" +#include "servers/rendering/rasterizer_rd/rasterizer_rd.h" + +#include "vulkan_context_iphone.h" + +#import <QuartzCore/CAMetalLayer.h> +#include <vulkan/vulkan_metal.h> +#endif + +class DisplayServerIPhone : public DisplayServer { + GDCLASS(DisplayServerIPhone, DisplayServer) + + _THREAD_SAFE_CLASS_ + +#if defined(VULKAN_ENABLED) + VulkanContextIPhone *context_vulkan; + RenderingDeviceVulkan *rendering_device_vulkan; +#endif + + DisplayServer::ScreenOrientation screen_orientation; + + ObjectID window_attached_instance_id; + + Callable window_event_callback; + Callable window_resize_callback; + Callable input_event_callback; + Callable input_text_callback; + + int virtual_keyboard_height = 0; + + void perform_event(const Ref<InputEvent> &p_event); + + DisplayServerIPhone(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + ~DisplayServerIPhone(); + +public: + String rendering_driver; + + static DisplayServerIPhone *get_singleton(); + + static void register_iphone_driver(); + static DisplayServer *create_func(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); + static Vector<String> get_rendering_drivers_func(); + + // MARK: - Events + + virtual void process_events() override; + + virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + + static void _dispatch_input_events(const Ref<InputEvent> &p_event); + void send_input_event(const Ref<InputEvent> &p_event) const; + void send_input_text(const String &p_text) const; + void send_window_event(DisplayServer::WindowEvent p_event) const; + void _window_callback(const Callable &p_callable, const Variant &p_arg) const; + + // MARK: - Input + + // MARK: Touches + + void touch_press(int p_idx, int p_x, int p_y, bool p_pressed, bool p_doubleclick); + void touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y); + void touches_cancelled(int p_idx); + + // MARK: Keyboard + + void key(uint32_t p_key, bool p_pressed); + + // MARK: Motion + + void update_gravity(float p_x, float p_y, float p_z); + void update_accelerometer(float p_x, float p_y, float p_z); + void update_magnetometer(float p_x, float p_y, float p_z); + void update_gyroscope(float p_x, float p_y, float p_z); + + // MARK: - + + virtual bool has_feature(Feature p_feature) const override; + virtual String get_name() const override; + + virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override; + + virtual int get_screen_count() 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_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + + virtual Vector<DisplayServer::WindowID> get_window_list() const override; + + virtual WindowID + get_window_at_screen_position(const Point2i &p_position) const override; + + virtual void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override; + virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override; + + virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID) override; + + virtual Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual 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_real_size(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 float screen_get_max_scale() const override; + + virtual void screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) override; + virtual DisplayServer::ScreenOrientation screen_get_orientation(int p_screen) const override; + + virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual bool can_any_window_draw() const override; + + virtual bool screen_is_touchscreen(int p_screen) const override; + + virtual void virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_length, int p_cursor_start, int p_cursor_end) override; + virtual void virtual_keyboard_hide() override; + + void virtual_keyboard_set_height(int height); + virtual int virtual_keyboard_get_height() const override; + + virtual void clipboard_set(const String &p_text) override; + virtual String clipboard_get() const override; + + virtual void screen_set_keep_on(bool p_enable) override; + virtual bool screen_is_kept_on() const override; + + virtual Error native_video_play(String p_path, float p_volume, String p_audio_track, String p_subtitle_track, int p_screen = SCREEN_OF_MAIN_WINDOW) override; + virtual bool native_video_is_playing() const override; + virtual void native_video_pause() override; + virtual void native_video_unpause() override; + virtual void native_video_stop() override; + + void resize_window(CGSize size); +}; + +#endif /* display_server_iphone_h */ diff --git a/platform/iphone/display_server_iphone.mm b/platform/iphone/display_server_iphone.mm new file mode 100644 index 0000000000..d456f9cbf6 --- /dev/null +++ b/platform/iphone/display_server_iphone.mm @@ -0,0 +1,641 @@ +/*************************************************************************/ +/* display_server_iphone.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "display_server_iphone.h" +#import "app_delegate.h" +#include "core/io/file_access_pack.h" +#include "core/project_settings.h" +#import "device_metrics.h" +#import "godot_view.h" +#include "ios.h" +#import "native_video_view.h" +#include "os_iphone.h" +#import "view_controller.h" + +#import <Foundation/Foundation.h> +#import <sys/utsname.h> + +static const float kDisplayServerIPhoneAcceleration = 1; + +DisplayServerIPhone *DisplayServerIPhone::get_singleton() { + return (DisplayServerIPhone *)DisplayServer::get_singleton(); +} + +DisplayServerIPhone::DisplayServerIPhone(const String &p_rendering_driver, DisplayServer::WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + rendering_driver = p_rendering_driver; + +#if defined(OPENGL_ENABLED) + // FIXME: Add support for both GLES2 and Vulkan when GLES2 is implemented + // again, + + if (rendering_driver == "opengl_es") { + bool gl_initialization_error = false; + + // FIXME: Add Vulkan support via MoltenVK. Add fallback code back? + + if (RasterizerGLES2::is_viable() == OK) { + RasterizerGLES2::register_config(); + RasterizerGLES2::make_current(); + } else { + gl_initialization_error = true; + } + + if (gl_initialization_error) { + OS::get_singleton()->alert("Your device does not support any of the supported OpenGL versions.", "Unable to initialize video driver"); + // return ERR_UNAVAILABLE; + } + + // rendering_server = memnew(RenderingServerRaster); + // // FIXME: Reimplement threaded rendering + // if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) { + // rendering_server = memnew(RenderingServerWrapMT(rendering_server, + // false)); + // } + // rendering_server->init(); + // rendering_server->cursor_set_visible(false, 0); + + // reset this to what it should be, it will have been set to 0 after + // rendering_server->init() is called + // RasterizerStorageGLES2::system_fbo = gl_view_base_fb; + } +#endif + +#if defined(VULKAN_ENABLED) + rendering_driver = "vulkan"; + + context_vulkan = nullptr; + rendering_device_vulkan = nullptr; + + if (rendering_driver == "vulkan") { + context_vulkan = memnew(VulkanContextIPhone); + if (context_vulkan->initialize() != OK) { + memdelete(context_vulkan); + context_vulkan = nullptr; + ERR_FAIL_MSG("Failed to initialize Vulkan context"); + } + + CALayer *layer = [AppDelegate.viewController.godotView initializeRenderingForDriver:@"vulkan"]; + + if (!layer) { + ERR_FAIL_MSG("Failed to create iOS rendering layer."); + } + + Size2i size = Size2i(layer.bounds.size.width, layer.bounds.size.height) * screen_get_max_scale(); + if (context_vulkan->window_create(MAIN_WINDOW_ID, layer, size.width, size.height) != OK) { + memdelete(context_vulkan); + context_vulkan = nullptr; + ERR_FAIL_MSG("Failed to create Vulkan window."); + } + + rendering_device_vulkan = memnew(RenderingDeviceVulkan); + rendering_device_vulkan->initialize(context_vulkan); + + RasterizerRD::make_current(); + } +#endif + + bool keep_screen_on = bool(GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true)); + screen_set_keep_on(keep_screen_on); + + Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events); + + r_error = OK; +} + +DisplayServerIPhone::~DisplayServerIPhone() { +#if defined(VULKAN_ENABLED) + if (rendering_driver == "vulkan") { + if (rendering_device_vulkan) { + rendering_device_vulkan->finalize(); + memdelete(rendering_device_vulkan); + rendering_device_vulkan = NULL; + } + + if (context_vulkan) { + context_vulkan->window_destroy(MAIN_WINDOW_ID); + memdelete(context_vulkan); + context_vulkan = NULL; + } + } +#endif +} + +DisplayServer *DisplayServerIPhone::create_func(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + return memnew(DisplayServerIPhone(p_rendering_driver, p_mode, p_flags, p_resolution, r_error)); +} + +Vector<String> DisplayServerIPhone::get_rendering_drivers_func() { + Vector<String> drivers; + +#if defined(VULKAN_ENABLED) + drivers.push_back("vulkan"); +#endif +#if defined(OPENGL_ENABLED) + drivers.push_back("opengl_es"); +#endif + + return drivers; +} + +void DisplayServerIPhone::register_iphone_driver() { + register_create_function("iphone", create_func, get_rendering_drivers_func); +} + +// MARK: Events + +void DisplayServerIPhone::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { + window_resize_callback = p_callable; +} + +void DisplayServerIPhone::window_set_window_event_callback(const Callable &p_callable, WindowID p_window) { + window_event_callback = p_callable; +} +void DisplayServerIPhone::window_set_input_event_callback(const Callable &p_callable, WindowID p_window) { + input_event_callback = p_callable; +} + +void DisplayServerIPhone::window_set_input_text_callback(const Callable &p_callable, WindowID p_window) { + input_text_callback = p_callable; +} + +void DisplayServerIPhone::window_set_drop_files_callback(const Callable &p_callable, WindowID p_window) { + // Probably not supported for iOS +} + +void DisplayServerIPhone::process_events() { +} + +void DisplayServerIPhone::_dispatch_input_events(const Ref<InputEvent> &p_event) { + DisplayServerIPhone::get_singleton()->send_input_event(p_event); +} + +void DisplayServerIPhone::send_input_event(const Ref<InputEvent> &p_event) const { + _window_callback(input_event_callback, p_event); +} + +void DisplayServerIPhone::send_input_text(const String &p_text) const { + _window_callback(input_text_callback, p_text); +} + +void DisplayServerIPhone::send_window_event(DisplayServer::WindowEvent p_event) const { + _window_callback(window_event_callback, int(p_event)); +} + +void DisplayServerIPhone::_window_callback(const Callable &p_callable, const Variant &p_arg) const { + if (!p_callable.is_null()) { + const Variant *argp = &p_arg; + Variant ret; + Callable::CallError ce; + p_callable.call((const Variant **)&argp, 1, ret, ce); + } +} + +// MARK: - Input + +// MARK: Touches + +void DisplayServerIPhone::touch_press(int p_idx, int p_x, int p_y, bool p_pressed, bool p_doubleclick) { + if (!GLOBAL_DEF("debug/disable_touch", false)) { + Ref<InputEventScreenTouch> ev; + ev.instance(); + + ev->set_index(p_idx); + ev->set_pressed(p_pressed); + ev->set_position(Vector2(p_x, p_y)); + perform_event(ev); + } +}; + +void DisplayServerIPhone::touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y) { + if (!GLOBAL_DEF("debug/disable_touch", false)) { + Ref<InputEventScreenDrag> ev; + ev.instance(); + ev->set_index(p_idx); + ev->set_position(Vector2(p_x, p_y)); + ev->set_relative(Vector2(p_x - p_prev_x, p_y - p_prev_y)); + perform_event(ev); + }; +}; + +void DisplayServerIPhone::perform_event(const Ref<InputEvent> &p_event) { + Input::get_singleton()->parse_input_event(p_event); +}; + +void DisplayServerIPhone::touches_cancelled(int p_idx) { + touch_press(p_idx, -1, -1, false, false); +}; + +// MARK: Keyboard + +void DisplayServerIPhone::key(uint32_t p_key, bool p_pressed) { + Ref<InputEventKey> ev; + ev.instance(); + ev->set_echo(false); + ev->set_pressed(p_pressed); + ev->set_keycode(p_key); + ev->set_physical_keycode(p_key); + ev->set_unicode(p_key); + perform_event(ev); +}; + +// MARK: Motion + +void DisplayServerIPhone::update_gravity(float p_x, float p_y, float p_z) { + Input::get_singleton()->set_gravity(Vector3(p_x, p_y, p_z)); +}; + +void DisplayServerIPhone::update_accelerometer(float p_x, float p_y, float p_z) { + // Found out the Z should not be negated! Pass as is! + Vector3 v_accelerometer = Vector3( + p_x / kDisplayServerIPhoneAcceleration, + p_y / kDisplayServerIPhoneAcceleration, + p_z / kDisplayServerIPhoneAcceleration); + + Input::get_singleton()->set_accelerometer(v_accelerometer); +}; + +void DisplayServerIPhone::update_magnetometer(float p_x, float p_y, float p_z) { + Input::get_singleton()->set_magnetometer(Vector3(p_x, p_y, p_z)); +}; + +void DisplayServerIPhone::update_gyroscope(float p_x, float p_y, float p_z) { + Input::get_singleton()->set_gyroscope(Vector3(p_x, p_y, p_z)); +}; + +// MARK: - + +bool DisplayServerIPhone::has_feature(Feature p_feature) const { + switch (p_feature) { + // case FEATURE_CONSOLE_WINDOW: + // case FEATURE_CURSOR_SHAPE: + // case FEATURE_CUSTOM_CURSOR_SHAPE: + // case FEATURE_GLOBAL_MENU: + // case FEATURE_HIDPI: + // case FEATURE_ICON: + // case FEATURE_IME: + // case FEATURE_MOUSE: + // case FEATURE_MOUSE_WARP: + // case FEATURE_NATIVE_DIALOG: + // case FEATURE_NATIVE_ICON: + // case FEATURE_NATIVE_VIDEO: + // case FEATURE_WINDOW_TRANSPARENCY: + case FEATURE_CLIPBOARD: + case FEATURE_KEEP_SCREEN_ON: + case FEATURE_ORIENTATION: + case FEATURE_TOUCHSCREEN: + case FEATURE_VIRTUAL_KEYBOARD: + return true; + default: + return false; + } +} + +String DisplayServerIPhone::get_name() const { + return "iPhone"; +} + +void DisplayServerIPhone::alert(const String &p_alert, const String &p_title) { + const CharString utf8_alert = p_alert.utf8(); + const CharString utf8_title = p_title.utf8(); + iOS::alert(utf8_alert.get_data(), utf8_title.get_data()); +} + +int DisplayServerIPhone::get_screen_count() const { + return 1; +} + +Point2i DisplayServerIPhone::screen_get_position(int p_screen) const { + return Size2i(); +} + +Size2i DisplayServerIPhone::screen_get_size(int p_screen) const { + CALayer *layer = AppDelegate.viewController.godotView.renderingLayer; + + if (!layer) { + return Size2i(); + } + + return Size2i(layer.bounds.size.width, layer.bounds.size.height) * screen_get_scale(p_screen); +} + +Rect2i DisplayServerIPhone::screen_get_usable_rect(int p_screen) const { + if (@available(iOS 11, *)) { + UIEdgeInsets insets = UIEdgeInsetsZero; + UIView *view = AppDelegate.viewController.godotView; + + if ([view respondsToSelector:@selector(safeAreaInsets)]) { + insets = [view safeAreaInsets]; + } + + float scale = screen_get_scale(p_screen); + Size2i insets_position = Size2i(insets.left, insets.top) * scale; + Size2i insets_size = Size2i(insets.left + insets.right, insets.top + insets.bottom) * scale; + + return Rect2i(screen_get_position(p_screen) + insets_position, screen_get_size(p_screen) - insets_size); + } else { + return Rect2i(screen_get_position(p_screen), screen_get_size(p_screen)); + } +} + +int DisplayServerIPhone::screen_get_dpi(int p_screen) const { + struct utsname systemInfo; + uname(&systemInfo); + + NSString *string = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; + + NSDictionary *iOSModelToDPI = [GodotDeviceMetrics dpiList]; + + for (NSArray *keyArray in iOSModelToDPI) { + if ([keyArray containsObject:string]) { + NSNumber *value = iOSModelToDPI[keyArray]; + return [value intValue]; + } + } + + // If device wasn't found in dictionary + // make a best guess from device metrics. + CGFloat scale = [UIScreen mainScreen].scale; + + UIUserInterfaceIdiom idiom = [UIDevice currentDevice].userInterfaceIdiom; + + switch (idiom) { + case UIUserInterfaceIdiomPad: + return scale == 2 ? 264 : 132; + case UIUserInterfaceIdiomPhone: { + if (scale == 3) { + CGFloat nativeScale = [UIScreen mainScreen].nativeScale; + return nativeScale == 3 ? 458 : 401; + } + + return 326; + } + default: + return 72; + } +} + +float DisplayServerIPhone::screen_get_scale(int p_screen) const { + return [UIScreen mainScreen].nativeScale; +} + +Vector<DisplayServer::WindowID> DisplayServerIPhone::get_window_list() const { + Vector<DisplayServer::WindowID> list; + list.push_back(MAIN_WINDOW_ID); + return list; +} + +DisplayServer::WindowID DisplayServerIPhone::get_window_at_screen_position(const Point2i &p_position) const { + return MAIN_WINDOW_ID; +} + +void DisplayServerIPhone::window_attach_instance_id(ObjectID p_instance, WindowID p_window) { + window_attached_instance_id = p_instance; +} + +ObjectID DisplayServerIPhone::window_get_attached_instance_id(WindowID p_window) const { + return window_attached_instance_id; +} + +void DisplayServerIPhone::window_set_title(const String &p_title, WindowID p_window) { + // Probably not supported for iOS +} + +int DisplayServerIPhone::window_get_current_screen(WindowID p_window) const { + return SCREEN_OF_MAIN_WINDOW; +} + +void DisplayServerIPhone::window_set_current_screen(int p_screen, WindowID p_window) { + // Probably not supported for iOS +} + +Point2i DisplayServerIPhone::window_get_position(WindowID p_window) const { + return Point2i(); +} + +void DisplayServerIPhone::window_set_position(const Point2i &p_position, WindowID p_window) { + // Probably not supported for single window iOS app +} + +void DisplayServerIPhone::window_set_transient(WindowID p_window, WindowID p_parent) { + // Probably not supported for iOS +} + +void DisplayServerIPhone::window_set_max_size(const Size2i p_size, WindowID p_window) { + // Probably not supported for iOS +} + +Size2i DisplayServerIPhone::window_get_max_size(WindowID p_window) const { + return Size2i(); +} + +void DisplayServerIPhone::window_set_min_size(const Size2i p_size, WindowID p_window) { + // Probably not supported for iOS +} + +Size2i DisplayServerIPhone::window_get_min_size(WindowID p_window) const { + return Size2i(); +} + +void DisplayServerIPhone::window_set_size(const Size2i p_size, WindowID p_window) { + // Probably not supported for iOS +} + +Size2i DisplayServerIPhone::window_get_size(WindowID p_window) const { + CGRect screenBounds = [UIScreen mainScreen].bounds; + return Size2i(screenBounds.size.width, screenBounds.size.height) * screen_get_max_scale(); +} + +Size2i DisplayServerIPhone::window_get_real_size(WindowID p_window) const { + return window_get_size(p_window); +} + +void DisplayServerIPhone::window_set_mode(WindowMode p_mode, WindowID p_window) { + // Probably not supported for iOS +} + +DisplayServer::WindowMode DisplayServerIPhone::window_get_mode(WindowID p_window) const { + return WindowMode::WINDOW_MODE_FULLSCREEN; +} + +bool DisplayServerIPhone::window_is_maximize_allowed(WindowID p_window) const { + return false; +} + +void DisplayServerIPhone::window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window) { + // Probably not supported for iOS +} + +bool DisplayServerIPhone::window_get_flag(WindowFlags p_flag, WindowID p_window) const { + return false; +} + +void DisplayServerIPhone::window_request_attention(WindowID p_window) { + // Probably not supported for iOS +} + +void DisplayServerIPhone::window_move_to_foreground(WindowID p_window) { + // Probably not supported for iOS +} + +float DisplayServerIPhone::screen_get_max_scale() const { + return screen_get_scale(SCREEN_OF_MAIN_WINDOW); +}; + +void DisplayServerIPhone::screen_set_orientation(DisplayServer::ScreenOrientation p_orientation, int p_screen) { + screen_orientation = p_orientation; +} + +DisplayServer::ScreenOrientation DisplayServerIPhone::screen_get_orientation(int p_screen) const { + return screen_orientation; +} + +bool DisplayServerIPhone::window_can_draw(WindowID p_window) const { + return true; +} + +bool DisplayServerIPhone::can_any_window_draw() const { + return true; +} + +bool DisplayServerIPhone::screen_is_touchscreen(int p_screen) const { + return true; +} + +void DisplayServerIPhone::virtual_keyboard_show(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_length, int p_cursor_start, int p_cursor_end) { + [AppDelegate.viewController.godotView becomeFirstResponderWithString:p_existing_text]; +} + +void DisplayServerIPhone::virtual_keyboard_hide() { + [AppDelegate.viewController.godotView resignFirstResponder]; +} + +void DisplayServerIPhone::virtual_keyboard_set_height(int height) { + virtual_keyboard_height = height * screen_get_max_scale(); +} + +int DisplayServerIPhone::virtual_keyboard_get_height() const { + return virtual_keyboard_height; +} + +void DisplayServerIPhone::clipboard_set(const String &p_text) { + [UIPasteboard generalPasteboard].string = [NSString stringWithUTF8String:p_text.utf8()]; +} + +String DisplayServerIPhone::clipboard_get() const { + NSString *text = [UIPasteboard generalPasteboard].string; + + return String::utf8([text UTF8String]); +} + +void DisplayServerIPhone::screen_set_keep_on(bool p_enable) { + [UIApplication sharedApplication].idleTimerDisabled = p_enable; +} + +bool DisplayServerIPhone::screen_is_kept_on() const { + return [UIApplication sharedApplication].idleTimerDisabled; +} + +Error DisplayServerIPhone::native_video_play(String p_path, float p_volume, String p_audio_track, String p_subtitle_track, int p_screen) { + FileAccess *f = FileAccess::open(p_path, FileAccess::READ); + bool exists = f && f->is_open(); + + String user_data_dir = OSIPhone::get_singleton()->get_user_data_dir(); + + if (!exists) { + return FAILED; + } + + String tempFile = OSIPhone::get_singleton()->get_user_data_dir(); + + if (p_path.begins_with("res://")) { + if (PackedData::get_singleton()->has_path(p_path)) { + printf("Unable to play %s using the native player as it resides in a .pck file\n", p_path.utf8().get_data()); + return ERR_INVALID_PARAMETER; + } else { + p_path = p_path.replace("res:/", ProjectSettings::get_singleton()->get_resource_path()); + } + } else if (p_path.begins_with("user://")) { + p_path = p_path.replace("user:/", user_data_dir); + } + + memdelete(f); + + printf("Playing video: %s\n", p_path.utf8().get_data()); + + String file_path = ProjectSettings::get_singleton()->globalize_path(p_path); + + NSString *filePath = [[NSString alloc] initWithUTF8String:file_path.utf8().get_data()]; + NSString *audioTrack = [NSString stringWithUTF8String:p_audio_track.utf8()]; + NSString *subtitleTrack = [NSString stringWithUTF8String:p_subtitle_track.utf8()]; + + if (![AppDelegate.viewController playVideoAtPath:filePath + volume:p_volume + audio:audioTrack + subtitle:subtitleTrack]) { + return OK; + } + + return FAILED; +} + +bool DisplayServerIPhone::native_video_is_playing() const { + return [AppDelegate.viewController.videoView isVideoPlaying]; +} + +void DisplayServerIPhone::native_video_pause() { + if (native_video_is_playing()) { + [AppDelegate.viewController.videoView pauseVideo]; + } +} + +void DisplayServerIPhone::native_video_unpause() { + [AppDelegate.viewController.videoView unpauseVideo]; +}; + +void DisplayServerIPhone::native_video_stop() { + if (native_video_is_playing()) { + [AppDelegate.viewController.videoView stopVideo]; + } +} + +void DisplayServerIPhone::resize_window(CGSize viewSize) { + Size2i size = Size2i(viewSize.width, viewSize.height) * screen_get_max_scale(); + +#if defined(VULKAN_ENABLED) + if (rendering_driver == "vulkan") { + if (context_vulkan) { + context_vulkan->window_resize(MAIN_WINDOW_ID, size.x, size.y); + } + } +#endif + + Variant resize_rect = Rect2i(Point2i(), size); + _window_callback(window_resize_callback, resize_rect); +} diff --git a/platform/iphone/export/export.cpp b/platform/iphone/export/export.cpp index 4a751488cb..19f7c8e482 100644 --- a/platform/iphone/export/export.cpp +++ b/platform/iphone/export/export.cpp @@ -86,6 +86,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { struct IOSExportAsset { String exported_path; bool is_framework; // framework is anything linked to the binary, otherwise it's a resource + bool should_embed; }; String _get_additional_plist_content(); @@ -100,7 +101,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { Vector<String> _get_preset_architectures(const Ref<EditorExportPreset> &p_preset); void _add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets); - Error _export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, Vector<IOSExportAsset> &r_exported_assets); + Error _export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets); Error _export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets); bool is_package_name_valid(const String &p_package, String *r_error = nullptr) const { @@ -114,7 +115,7 @@ class EditorExportPlatformIOS : public EditorExportPlatform { } for (int i = 0; i < pname.length(); i++) { - CharType c = pname[i]; + char32_t c = pname[i]; if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c)); @@ -159,9 +160,8 @@ public: void EditorExportPlatformIOS::get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) { String driver = ProjectSettings::get_singleton()->get("rendering/quality/driver/driver_name"); - if (driver == "GLES2") { - r_features->push_back("etc"); - } else if (driver == "Vulkan") { + r_features->push_back("pvrtc"); + if (driver == "Vulkan") { // FIXME: Review if this is correct. r_features->push_back("etc2"); } @@ -912,15 +912,6 @@ struct ExportLibsData { }; void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPreset> &p_preset, Vector<uint8_t> &p_project_data, const Vector<IOSExportAsset> &p_additional_assets) { - Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); - Vector<String> frameworks; - for (int i = 0; i < export_plugins.size(); ++i) { - Vector<String> plugin_frameworks = export_plugins[i]->get_ios_frameworks(); - for (int j = 0; j < plugin_frameworks.size(); ++j) { - frameworks.push_back(plugin_frameworks[j]); - } - } - // that is just a random number, we just need Godot IDs not to clash with // existing IDs in the project. PbxId current_id = { 0x58938401, 0, 0 }; @@ -945,15 +936,19 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese String type; if (asset.exported_path.ends_with(".framework")) { - additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; - framework_id = (++current_id).str(); - pbx_embeded_frameworks += framework_id + ",\n"; + if (asset.should_embed) { + additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; + framework_id = (++current_id).str(); + pbx_embeded_frameworks += framework_id + ",\n"; + } type = "wrapper.framework"; } else if (asset.exported_path.ends_with(".xcframework")) { - additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; - framework_id = (++current_id).str(); - pbx_embeded_frameworks += framework_id + ",\n"; + if (asset.should_embed) { + additional_asset_info_format += "$framework_id = {isa = PBXBuildFile; fileRef = $ref_id; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };\n"; + framework_id = (++current_id).str(); + pbx_embeded_frameworks += framework_id + ",\n"; + } type = "wrapper.xcframework"; } else if (asset.exported_path.ends_with(".dylib")) { @@ -1026,7 +1021,7 @@ void EditorExportPlatformIOS::_add_assets_to_project(const Ref<EditorExportPrese } } -Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, Vector<IOSExportAsset> &r_exported_assets) { +Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<String> &p_assets, bool p_is_framework, bool p_should_embed, Vector<IOSExportAsset> &r_exported_assets) { DirAccess *filesystem_da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); String binary_name = p_out_dir.get_file().get_basename(); @@ -1035,7 +1030,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir String asset = p_assets[f_idx]; if (!asset.begins_with("res://")) { // either SDK-builtin or already a part of the export template - IOSExportAsset exported_asset = { asset, p_is_framework }; + IOSExportAsset exported_asset = { asset, p_is_framework, p_should_embed }; r_exported_assets.push_back(exported_asset); } else { DirAccess *da = DirAccess::create_for_path(asset); @@ -1101,7 +1096,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir memdelete(filesystem_da); return err; } - IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework }; + IOSExportAsset exported_asset = { binary_name.plus_file(asset_path), p_is_framework, p_should_embed }; r_exported_assets.push_back(exported_asset); if (create_framework) { @@ -1165,19 +1160,23 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir, const Vector<SharedObject> &p_libraries, Vector<IOSExportAsset> &r_exported_assets) { Vector<Ref<EditorExportPlugin>> export_plugins = EditorExport::get_singleton()->get_export_plugins(); for (int i = 0; i < export_plugins.size(); i++) { - Vector<String> frameworks = export_plugins[i]->get_ios_frameworks(); - Error err = _export_additional_assets(p_out_dir, frameworks, true, r_exported_assets); + Vector<String> linked_frameworks = export_plugins[i]->get_ios_frameworks(); + Error err = _export_additional_assets(p_out_dir, linked_frameworks, true, false, r_exported_assets); + ERR_FAIL_COND_V(err, err); + + Vector<String> embedded_frameworks = export_plugins[i]->get_ios_embedded_frameworks(); + err = _export_additional_assets(p_out_dir, embedded_frameworks, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); Vector<String> project_static_libs = export_plugins[i]->get_ios_project_static_libs(); for (int j = 0; j < project_static_libs.size(); j++) { project_static_libs.write[j] = project_static_libs[j].get_file(); // Only the file name as it's copied to the project } - err = _export_additional_assets(p_out_dir, project_static_libs, true, r_exported_assets); + err = _export_additional_assets(p_out_dir, project_static_libs, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); Vector<String> ios_bundle_files = export_plugins[i]->get_ios_bundle_files(); - err = _export_additional_assets(p_out_dir, ios_bundle_files, false, r_exported_assets); + err = _export_additional_assets(p_out_dir, ios_bundle_files, false, false, r_exported_assets); ERR_FAIL_COND_V(err, err); } @@ -1185,7 +1184,7 @@ Error EditorExportPlatformIOS::_export_additional_assets(const String &p_out_dir for (int i = 0; i < p_libraries.size(); ++i) { library_paths.push_back(p_libraries[i].path); } - Error err = _export_additional_assets(p_out_dir, library_paths, true, r_exported_assets); + Error err = _export_additional_assets(p_out_dir, library_paths, true, true, r_exported_assets); ERR_FAIL_COND_V(err, err); return OK; @@ -1649,7 +1648,7 @@ bool EditorExportPlatformIOS::can_export(const Ref<EditorExportPreset> &p_preset } } - String etc_error = test_etc2(); + String etc_error = test_etc2_or_pvrtc(); if (etc_error != String()) { valid = false; err += etc_error; diff --git a/platform/iphone/game_center.h b/platform/iphone/game_center.h index 0d3ef5b696..10c8ef52fb 100644 --- a/platform/iphone/game_center.h +++ b/platform/iphone/game_center.h @@ -33,7 +33,7 @@ #ifndef GAME_CENTER_H #define GAME_CENTER_H -#include "core/object.h" +#include "core/class_db.h" class GameCenter : public Object { GDCLASS(GameCenter, Object); @@ -48,15 +48,15 @@ class GameCenter : public Object { void return_connect_error(const char *p_error_description); public: - void connect(); + Error authenticate(); bool is_authenticated(); - Error post_score(Variant p_score); - Error award_achievement(Variant p_params); + Error post_score(Dictionary p_score); + Error award_achievement(Dictionary p_params); void reset_achievements(); void request_achievements(); void request_achievement_descriptions(); - Error show_game_center(Variant p_params); + Error show_game_center(Dictionary p_params); Error request_identity_verification_signature(); void game_center_closed(); diff --git a/platform/iphone/game_center.mm b/platform/iphone/game_center.mm index 8d470da1a8..0f8c0100c3 100644 --- a/platform/iphone/game_center.mm +++ b/platform/iphone/game_center.mm @@ -47,13 +47,16 @@ extern "C" { #import "app_delegate.h" }; +#import "view_controller.h" + GameCenter *GameCenter::instance = NULL; void GameCenter::_bind_methods() { + ClassDB::bind_method(D_METHOD("authenticate"), &GameCenter::authenticate); ClassDB::bind_method(D_METHOD("is_authenticated"), &GameCenter::is_authenticated); ClassDB::bind_method(D_METHOD("post_score"), &GameCenter::post_score); - ClassDB::bind_method(D_METHOD("award_achievement"), &GameCenter::award_achievement); + ClassDB::bind_method(D_METHOD("award_achievement", "achievement"), &GameCenter::award_achievement); ClassDB::bind_method(D_METHOD("reset_achievements"), &GameCenter::reset_achievements); ClassDB::bind_method(D_METHOD("request_achievements"), &GameCenter::request_achievements); ClassDB::bind_method(D_METHOD("request_achievement_descriptions"), &GameCenter::request_achievement_descriptions); @@ -64,40 +67,28 @@ void GameCenter::_bind_methods() { ClassDB::bind_method(D_METHOD("pop_pending_event"), &GameCenter::pop_pending_event); }; -void GameCenter::return_connect_error(const char *p_error_description) { - authenticated = false; - Dictionary ret; - ret["type"] = "authentication"; - ret["result"] = "error"; - ret["error_code"] = 0; - ret["error_description"] = p_error_description; - pending_events.push_back(ret); -} - -void GameCenter::connect() { +Error GameCenter::authenticate() { //if this class isn't available, game center isn't implemented if ((NSClassFromString(@"GKLocalPlayer")) == nil) { - return_connect_error("GameCenter not available"); - return; + return ERR_UNAVAILABLE; } GKLocalPlayer *player = [GKLocalPlayer localPlayer]; - if (![player respondsToSelector:@selector(authenticateHandler)]) { - return_connect_error("GameCenter doesn't respond to 'authenticateHandler'"); - return; - } + ERR_FAIL_COND_V(![player respondsToSelector:@selector(authenticateHandler)], ERR_UNAVAILABLE); ViewController *root_controller = (ViewController *)((AppDelegate *)[[UIApplication sharedApplication] delegate]).window.rootViewController; - if (!root_controller) { - return_connect_error("Window doesn't have root ViewController"); - return; - } + ERR_FAIL_COND_V(!root_controller, FAILED); // This handler is called several times. First when the view needs to be shown, then again // after the view is cancelled or the user logs in. Or if the user's already logged in, it's // called just once to confirm they're authenticated. This is why no result needs to be specified // in the presentViewController phase. In this case, more calls to this function will follow. + _weakify(root_controller); + _weakify(player); player.authenticateHandler = (^(UIViewController *controller, NSError *error) { + _strongify(root_controller); + _strongify(player); + if (controller) { [root_controller presentViewController:controller animated:YES completion:nil]; } else { @@ -105,7 +96,14 @@ void GameCenter::connect() { ret["type"] = "authentication"; if (player.isAuthenticated) { ret["result"] = "ok"; - ret["player_id"] = [player.playerID UTF8String]; + if (@available(iOS 13, *)) { + ret["player_id"] = [player.teamPlayerID UTF8String]; +#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR + } else { + ret["player_id"] = [player.playerID UTF8String]; +#endif + } + GameCenter::get_singleton()->authenticated = true; } else { ret["result"] = "error"; @@ -117,20 +115,21 @@ void GameCenter::connect() { pending_events.push_back(ret); }; }); + + return OK; }; bool GameCenter::is_authenticated() { return authenticated; }; -Error GameCenter::post_score(Variant p_score) { - Dictionary params = p_score; - ERR_FAIL_COND_V(!params.has("score") || !params.has("category"), ERR_INVALID_PARAMETER); - float score = params["score"]; - String category = params["category"]; +Error GameCenter::post_score(Dictionary p_score) { + ERR_FAIL_COND_V(!p_score.has("score") || !p_score.has("category"), ERR_INVALID_PARAMETER); + float score = p_score["score"]; + String category = p_score["category"]; - NSString *cat_str = [[[NSString alloc] initWithUTF8String:category.utf8().get_data()] autorelease]; - GKScore *reporter = [[[GKScore alloc] initWithLeaderboardIdentifier:cat_str] autorelease]; + NSString *cat_str = [[NSString alloc] initWithUTF8String:category.utf8().get_data()]; + GKScore *reporter = [[GKScore alloc] initWithLeaderboardIdentifier:cat_str]; reporter.value = score; ERR_FAIL_COND_V([GKScore respondsToSelector:@selector(reportScores)], ERR_UNAVAILABLE); @@ -153,22 +152,21 @@ Error GameCenter::post_score(Variant p_score) { return OK; }; -Error GameCenter::award_achievement(Variant p_params) { - Dictionary params = p_params; - ERR_FAIL_COND_V(!params.has("name") || !params.has("progress"), ERR_INVALID_PARAMETER); - String name = params["name"]; - float progress = params["progress"]; +Error GameCenter::award_achievement(Dictionary p_params) { + ERR_FAIL_COND_V(!p_params.has("name") || !p_params.has("progress"), ERR_INVALID_PARAMETER); + String name = p_params["name"]; + float progress = p_params["progress"]; - NSString *name_str = [[[NSString alloc] initWithUTF8String:name.utf8().get_data()] autorelease]; - GKAchievement *achievement = [[[GKAchievement alloc] initWithIdentifier:name_str] autorelease]; + NSString *name_str = [[NSString alloc] initWithUTF8String:name.utf8().get_data()]; + GKAchievement *achievement = [[GKAchievement alloc] initWithIdentifier:name_str]; ERR_FAIL_COND_V(!achievement, FAILED); ERR_FAIL_COND_V([GKAchievement respondsToSelector:@selector(reportAchievements)], ERR_UNAVAILABLE); achievement.percentComplete = progress; achievement.showsCompletionBanner = NO; - if (params.has("show_completion_banner")) { - achievement.showsCompletionBanner = params["show_completion_banner"] ? YES : NO; + if (p_params.has("show_completion_banner")) { + achievement.showsCompletionBanner = p_params["show_completion_banner"] ? YES : NO; } [GKAchievement reportAchievements:@[ achievement ] @@ -202,7 +200,7 @@ void GameCenter::request_achievement_descriptions() { Array hidden; Array replayable; - for (int i = 0; i < [descriptions count]; i++) { + for (NSUInteger i = 0; i < [descriptions count]; i++) { GKAchievementDescription *description = [descriptions objectAtIndex:i]; const char *str = [description.identifier UTF8String]; @@ -250,7 +248,7 @@ void GameCenter::request_achievements() { PackedStringArray names; PackedFloat32Array percentages; - for (int i = 0; i < [achievements count]; i++) { + for (NSUInteger i = 0; i < [achievements count]; i++) { GKAchievement *achievement = [achievements objectAtIndex:i]; const char *str = [achievement.identifier UTF8String]; names.push_back(String::utf8(str != NULL ? str : "")); @@ -285,14 +283,12 @@ void GameCenter::reset_achievements() { }]; }; -Error GameCenter::show_game_center(Variant p_params) { +Error GameCenter::show_game_center(Dictionary p_params) { ERR_FAIL_COND_V(!NSProtocolFromString(@"GKGameCenterControllerDelegate"), FAILED); - Dictionary params = p_params; - GKGameCenterViewControllerState view_state = GKGameCenterViewControllerStateDefault; - if (params.has("view")) { - String view_name = params["view"]; + if (p_params.has("view")) { + String view_name = p_params["view"]; if (view_name == "default") { view_state = GKGameCenterViewControllerStateDefault; } else if (view_name == "leaderboards") { @@ -316,9 +312,9 @@ Error GameCenter::show_game_center(Variant p_params) { controller.viewState = view_state; if (view_state == GKGameCenterViewControllerStateLeaderboards) { controller.leaderboardIdentifier = nil; - if (params.has("leaderboard_name")) { - String name = params["leaderboard_name"]; - NSString *name_str = [[[NSString alloc] initWithUTF8String:name.utf8().get_data()] autorelease]; + if (p_params.has("leaderboard_name")) { + String name = p_params["leaderboard_name"]; + NSString *name_str = [[NSString alloc] initWithUTF8String:name.utf8().get_data()]; controller.leaderboardIdentifier = name_str; } } @@ -341,7 +337,13 @@ Error GameCenter::request_identity_verification_signature() { ret["signature"] = [[signature base64EncodedStringWithOptions:0] UTF8String]; ret["salt"] = [[salt base64EncodedStringWithOptions:0] UTF8String]; ret["timestamp"] = timestamp; - ret["player_id"] = [player.playerID UTF8String]; + if (@available(iOS 13, *)) { + ret["player_id"] = [player.teamPlayerID UTF8String]; +#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR + } else { + ret["player_id"] = [player.playerID UTF8String]; +#endif + } } else { ret["result"] = "error"; ret["error_code"] = (int64_t)error.code; diff --git a/platform/iphone/gl_view.h b/platform/iphone/gl_view.h deleted file mode 100644 index 975aa4b70a..0000000000 --- a/platform/iphone/gl_view.h +++ /dev/null @@ -1,123 +0,0 @@ -/*************************************************************************/ -/* gl_view.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 <AVFoundation/AVFoundation.h> -#import <MediaPlayer/MediaPlayer.h> -#import <OpenGLES/EAGL.h> -#import <OpenGLES/ES1/gl.h> -#import <OpenGLES/ES1/glext.h> -#import <UIKit/UIKit.h> - -@protocol GLViewDelegate; - -@interface GLView : UIView <UIKeyInput> { -@private - // The pixel dimensions of the backbuffer - GLint backingWidth; - GLint backingHeight; - - EAGLContext *context; - - // OpenGL names for the renderbuffer and framebuffers used to render to this view - GLuint viewRenderbuffer, viewFramebuffer; - - // OpenGL name for the depth buffer that is attached to viewFramebuffer, if it exists (0 if it does not exist) - GLuint depthRenderbuffer; - - BOOL useCADisplayLink; - // CADisplayLink available on 3.1+ synchronizes the animation timer & drawing with the refresh rate of the display, only supports animation intervals of 1/60 1/30 & 1/15 - CADisplayLink *displayLink; - - // An animation timer that, when animation is started, will periodically call -drawView at the given rate. - // Only used if CADisplayLink is not - NSTimer *animationTimer; - - NSTimeInterval animationInterval; - - // Delegate to do our drawing, called by -drawView, which can be called manually or via the animation timer. - id<GLViewDelegate> delegate; - - // Flag to denote that the -setupView method of a delegate has been called. - // Resets to NO whenever the delegate changes. - BOOL delegateSetup; - BOOL active; - float screen_scale; -} - -@property(nonatomic, assign) id<GLViewDelegate> delegate; - -// AVPlayer-related properties -@property(strong, nonatomic) AVAsset *avAsset; -@property(strong, nonatomic) AVPlayerItem *avPlayerItem; -@property(strong, nonatomic) AVPlayer *avPlayer; -@property(strong, nonatomic) AVPlayerLayer *avPlayerLayer; - -@property(strong, nonatomic) UIWindow *backgroundWindow; - -@property(nonatomic) UITextAutocorrectionType autocorrectionType; - -- (void)startAnimation; -- (void)stopAnimation; -- (void)drawView; - -- (BOOL)canBecomeFirstResponder; - -- (void)open_keyboard; -- (void)hide_keyboard; -- (void)deleteBackward; -- (BOOL)hasText; -- (void)insertText:(NSString *)p_text; - -- (id)initGLES; -- (BOOL)createFramebuffer; -- (void)destroyFramebuffer; - -- (void)audioRouteChangeListenerCallback:(NSNotification *)notification; -- (void)keyboardOnScreen:(NSNotification *)notification; -- (void)keyboardHidden:(NSNotification *)notification; - -@property NSTimeInterval animationInterval; -@property(nonatomic, assign) BOOL useCADisplayLink; - -@end - -@protocol GLViewDelegate <NSObject> - -@required - -// Draw with OpenGL ES -- (void)drawView:(GLView *)view; - -@optional - -// Called whenever you need to do some initialization before rendering. -- (void)setupView:(GLView *)view; - -@end diff --git a/platform/iphone/gl_view.mm b/platform/iphone/gl_view.mm deleted file mode 100644 index 1169ebc6b4..0000000000 --- a/platform/iphone/gl_view.mm +++ /dev/null @@ -1,702 +0,0 @@ -/*************************************************************************/ -/* gl_view.mm */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 "gl_view.h" - -#include "core/os/keyboard.h" -#include "core/project_settings.h" -#include "os_iphone.h" -#include "servers/audio_server.h" - -#import <OpenGLES/EAGLDrawable.h> -#import <QuartzCore/QuartzCore.h> - -/* -@interface GLView (private) - -- (id)initGLES; -- (BOOL)createFramebuffer; -- (void)destroyFramebuffer; -@end -*/ - -int gl_view_base_fb; -static String keyboard_text; -static GLView *_instance = NULL; - -static bool video_found_error = false; -static bool video_playing = false; -static CMTime video_current_time; - -void _show_keyboard(String); -void _hide_keyboard(); -bool _play_video(String, float, String, String); -bool _is_video_playing(); -void _pause_video(); -void _focus_out_video(); -void _unpause_video(); -void _stop_video(); -CGFloat _points_to_pixels(CGFloat); - -void _show_keyboard(String p_existing) { - keyboard_text = p_existing; - printf("instance on show is %p\n", _instance); - [_instance open_keyboard]; -}; - -void _hide_keyboard() { - printf("instance on hide is %p\n", _instance); - [_instance hide_keyboard]; - keyboard_text = ""; -}; - -Rect2 _get_ios_window_safe_area(float p_window_width, float p_window_height) { - UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, 0, 0); - if (_instance != nil && [_instance respondsToSelector:@selector(safeAreaInsets)]) { - insets = [_instance safeAreaInsets]; - } - ERR_FAIL_COND_V(insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0, - Rect2(0, 0, p_window_width, p_window_height)); - UIEdgeInsets window_insets = UIEdgeInsetsMake(_points_to_pixels(insets.top), _points_to_pixels(insets.left), _points_to_pixels(insets.bottom), _points_to_pixels(insets.right)); - return Rect2(window_insets.left, window_insets.top, p_window_width - window_insets.right - window_insets.left, p_window_height - window_insets.bottom - window_insets.top); -} - -bool _play_video(String p_path, float p_volume, String p_audio_track, String p_subtitle_track) { - p_path = ProjectSettings::get_singleton()->globalize_path(p_path); - - NSString *file_path = [[[NSString alloc] initWithUTF8String:p_path.utf8().get_data()] autorelease]; - - _instance.avAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:file_path]]; - - _instance.avPlayerItem = [[AVPlayerItem alloc] initWithAsset:_instance.avAsset]; - [_instance.avPlayerItem addObserver:_instance forKeyPath:@"status" options:0 context:nil]; - - _instance.avPlayer = [[AVPlayer alloc] initWithPlayerItem:_instance.avPlayerItem]; - _instance.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:_instance.avPlayer]; - - [_instance.avPlayer addObserver:_instance forKeyPath:@"status" options:0 context:nil]; - [[NSNotificationCenter defaultCenter] - addObserver:_instance - selector:@selector(playerItemDidReachEnd:) - name:AVPlayerItemDidPlayToEndTimeNotification - object:[_instance.avPlayer currentItem]]; - - [_instance.avPlayer addObserver:_instance forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:0]; - - [_instance.avPlayerLayer setFrame:_instance.bounds]; - [_instance.layer addSublayer:_instance.avPlayerLayer]; - [_instance.avPlayer play]; - - AVMediaSelectionGroup *audioGroup = [_instance.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; - - NSMutableArray *allAudioParams = [NSMutableArray array]; - for (id track in audioGroup.options) { - NSString *language = [[track locale] localeIdentifier]; - NSLog(@"subtitle lang: %@", language); - - if ([language isEqualToString:[NSString stringWithUTF8String:p_audio_track.utf8()]]) { - AVMutableAudioMixInputParameters *audioInputParams = [AVMutableAudioMixInputParameters audioMixInputParameters]; - [audioInputParams setVolume:p_volume atTime:kCMTimeZero]; - [audioInputParams setTrackID:[track trackID]]; - [allAudioParams addObject:audioInputParams]; - - AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix]; - [audioMix setInputParameters:allAudioParams]; - - [_instance.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:audioGroup]; - [_instance.avPlayer.currentItem setAudioMix:audioMix]; - - break; - } - } - - AVMediaSelectionGroup *subtitlesGroup = [_instance.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; - NSArray *useableTracks = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitlesGroup.options withoutMediaCharacteristics:[NSArray arrayWithObject:AVMediaCharacteristicContainsOnlyForcedSubtitles]]; - - for (id track in useableTracks) { - NSString *language = [[track locale] localeIdentifier]; - NSLog(@"subtitle lang: %@", language); - - if ([language isEqualToString:[NSString stringWithUTF8String:p_subtitle_track.utf8()]]) { - [_instance.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:subtitlesGroup]; - break; - } - } - - video_playing = true; - - return true; -} - -bool _is_video_playing() { - if (_instance.avPlayer.error) { - printf("Error during playback\n"); - } - return (_instance.avPlayer.rate > 0 && !_instance.avPlayer.error); -} - -void _pause_video() { - video_current_time = _instance.avPlayer.currentTime; - [_instance.avPlayer pause]; - video_playing = false; -} - -void _focus_out_video() { - printf("focus out pausing video\n"); - [_instance.avPlayer pause]; -}; - -void _unpause_video() { - [_instance.avPlayer play]; - video_playing = true; -}; - -void _stop_video() { - [_instance.avPlayer pause]; - [_instance.avPlayerLayer removeFromSuperlayer]; - _instance.avPlayer = nil; - video_playing = false; -} - -CGFloat _points_to_pixels(CGFloat points) { - float pixelPerInch; - if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { - pixelPerInch = 132; - } else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { - pixelPerInch = 163; - } else { - pixelPerInch = 160; - } - CGFloat pointsPerInch = 72.0; - return (points / pointsPerInch * pixelPerInch); -} - -@implementation GLView - -@synthesize animationInterval; - -static const int max_touches = 8; -static UITouch *touches[max_touches]; - -static void init_touches() { - for (int i = 0; i < max_touches; i++) { - touches[i] = NULL; - }; -}; - -static int get_touch_id(UITouch *p_touch) { - int first = -1; - for (int i = 0; i < max_touches; i++) { - if (first == -1 && touches[i] == NULL) { - first = i; - continue; - }; - if (touches[i] == p_touch) - return i; - }; - - if (first != -1) { - touches[first] = p_touch; - return first; - }; - - return -1; -}; - -static int remove_touch(UITouch *p_touch) { - int remaining = 0; - for (int i = 0; i < max_touches; i++) { - if (touches[i] == NULL) - continue; - if (touches[i] == p_touch) - touches[i] = NULL; - else - ++remaining; - }; - return remaining; -}; - -static void clear_touches() { - for (int i = 0; i < max_touches; i++) { - touches[i] = NULL; - }; -}; - -// Implement this to override the default layer class (which is [CALayer class]). -// We do this so that our view will be backed by a layer that is capable of OpenGL ES rendering. -+ (Class)layerClass { - return [CAEAGLLayer class]; -} - -//The GL view is stored in the nib file. When it's unarchived it's sent -initWithCoder: -- (id)initWithCoder:(NSCoder *)coder { - active = FALSE; - if ((self = [super initWithCoder:coder])) { - self = [self initGLES]; - } - return self; -} - -- (id)initGLES { - // Get our backing layer - CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer; - - // Configure it so that it is opaque, does not retain the contents of the backbuffer when displayed, and uses RGBA8888 color. - eaglLayer.opaque = YES; - eaglLayer.drawableProperties = [NSDictionary - dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:FALSE], - kEAGLDrawablePropertyRetainedBacking, - kEAGLColorFormatRGBA8, - kEAGLDrawablePropertyColorFormat, - nil]; - - // FIXME: Add Vulkan support via MoltenVK. Add fallback code back? - - // Create GL ES 2 context - if (GLOBAL_GET("rendering/quality/driver/driver_name") == "GLES2") { - context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; - NSLog(@"Setting up an OpenGL ES 2.0 context."); - if (!context) { - NSLog(@"Failed to create OpenGL ES 2.0 context!"); - return nil; - } - } - - if (![EAGLContext setCurrentContext:context]) { - NSLog(@"Failed to set EAGLContext!"); - return nil; - } - if (![self createFramebuffer]) { - NSLog(@"Failed to create frame buffer!"); - return nil; - } - - // Default the animation interval to 1/60th of a second. - animationInterval = 1.0 / 60.0; - return self; -} - -- (id<GLViewDelegate>)delegate { - return delegate; -} - -// Update the delegate, and if it needs a -setupView: call, set our internal flag so that it will be called. -- (void)setDelegate:(id<GLViewDelegate>)d { - delegate = d; - delegateSetup = ![delegate respondsToSelector:@selector(setupView:)]; -} - -@synthesize useCADisplayLink; - -// If our view is resized, we'll be asked to layout subviews. -// This is the perfect opportunity to also update the framebuffer so that it is -// the same size as our display area. - -- (void)layoutSubviews { - [EAGLContext setCurrentContext:context]; - [self destroyFramebuffer]; - [self createFramebuffer]; - [self drawView]; -} - -- (BOOL)createFramebuffer { - // Generate IDs for a framebuffer object and a color renderbuffer - UIScreen *mainscr = [UIScreen mainScreen]; - printf("******** screen size %i, %i\n", (int)mainscr.currentMode.size.width, (int)mainscr.currentMode.size.height); - self.contentScaleFactor = mainscr.nativeScale; - - glGenFramebuffersOES(1, &viewFramebuffer); - glGenRenderbuffersOES(1, &viewRenderbuffer); - - glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer); - glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer); - // This call associates the storage for the current render buffer with the EAGLDrawable (our CAEAGLLayer) - // allowing us to draw into a buffer that will later be rendered to screen wherever the layer is (which corresponds with our view). - [context renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:(id<EAGLDrawable>)self.layer]; - glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES, GL_RENDERBUFFER_OES, viewRenderbuffer); - - glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_WIDTH_OES, &backingWidth); - glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, GL_RENDERBUFFER_HEIGHT_OES, &backingHeight); - - // For this sample, we also need a depth buffer, so we'll create and attach one via another renderbuffer. - glGenRenderbuffersOES(1, &depthRenderbuffer); - glBindRenderbufferOES(GL_RENDERBUFFER_OES, depthRenderbuffer); - glRenderbufferStorageOES(GL_RENDERBUFFER_OES, GL_DEPTH_COMPONENT16_OES, backingWidth, backingHeight); - glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, GL_DEPTH_ATTACHMENT_OES, GL_RENDERBUFFER_OES, depthRenderbuffer); - - if (glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES) != GL_FRAMEBUFFER_COMPLETE_OES) { - NSLog(@"failed to make complete framebuffer object %x", glCheckFramebufferStatusOES(GL_FRAMEBUFFER_OES)); - return NO; - } - - if (OS::get_singleton()) { - OS::VideoMode vm; - vm.fullscreen = true; - vm.width = backingWidth; - vm.height = backingHeight; - vm.resizable = false; - OS::get_singleton()->set_video_mode(vm); - OSIPhone::get_singleton()->set_base_framebuffer(viewFramebuffer); - }; - gl_view_base_fb = viewFramebuffer; - - return YES; -} - -// Clean up any buffers we have allocated. -- (void)destroyFramebuffer { - glDeleteFramebuffersOES(1, &viewFramebuffer); - viewFramebuffer = 0; - glDeleteRenderbuffersOES(1, &viewRenderbuffer); - viewRenderbuffer = 0; - - if (depthRenderbuffer) { - glDeleteRenderbuffersOES(1, &depthRenderbuffer); - depthRenderbuffer = 0; - } -} - -- (void)startAnimation { - if (active) - return; - active = TRUE; - printf("start animation!\n"); - if (useCADisplayLink) { - // Approximate frame rate - // assumes device refreshes at 60 fps - int frameInterval = (int)floor(animationInterval * 60.0f); - - displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView)]; - [displayLink setFrameInterval:frameInterval]; - - // Setup DisplayLink in main thread - [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - } else { - animationTimer = [NSTimer scheduledTimerWithTimeInterval:animationInterval target:self selector:@selector(drawView) userInfo:nil repeats:YES]; - } - - if (video_playing) { - _unpause_video(); - } -} - -- (void)stopAnimation { - if (!active) - return; - active = FALSE; - printf("******** stop animation!\n"); - - if (useCADisplayLink) { - [displayLink invalidate]; - displayLink = nil; - } else { - [animationTimer invalidate]; - animationTimer = nil; - } - - clear_touches(); - - if (video_playing) { - // save position - } -} - -- (void)setAnimationInterval:(NSTimeInterval)interval { - animationInterval = interval; - if ((useCADisplayLink && displayLink) || (!useCADisplayLink && animationTimer)) { - [self stopAnimation]; - [self startAnimation]; - } -} - -// Updates the OpenGL view when the timer fires -- (void)drawView { - if (!active) { - printf("draw view not active!\n"); - return; - }; - if (useCADisplayLink) { - // Pause the CADisplayLink to avoid recursion - [displayLink setPaused:YES]; - - // Process all input events - while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource) - ; - - // We are good to go, resume the CADisplayLink - [displayLink setPaused:NO]; - } - - // Make sure that you are drawing to the current context - [EAGLContext setCurrentContext:context]; - - // If our drawing delegate needs to have the view setup, then call -setupView: and flag that it won't need to be called again. - if (!delegateSetup) { - [delegate setupView:self]; - delegateSetup = YES; - } - - glBindFramebufferOES(GL_FRAMEBUFFER_OES, viewFramebuffer); - - [delegate drawView:self]; - - glBindRenderbufferOES(GL_RENDERBUFFER_OES, viewRenderbuffer); - [context presentRenderbuffer:GL_RENDERBUFFER_OES]; - -#ifdef DEBUG_ENABLED - GLenum err = glGetError(); - if (err) - NSLog(@"DrawView: %x error", err); -#endif -} - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - NSArray *tlist = [[event allTouches] allObjects]; - for (unsigned int i = 0; i < [tlist count]; i++) { - if ([touches containsObject:[tlist objectAtIndex:i]]) { - UITouch *touch = [tlist objectAtIndex:i]; - if (touch.phase != UITouchPhaseBegan) - continue; - int tid = get_touch_id(touch); - ERR_FAIL_COND(tid == -1); - CGPoint touchPoint = [touch locationInView:self]; - OSIPhone::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, true, touch.tapCount > 1); - }; - }; -} - -- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { - NSArray *tlist = [[event allTouches] allObjects]; - for (unsigned int i = 0; i < [tlist count]; i++) { - if ([touches containsObject:[tlist objectAtIndex:i]]) { - UITouch *touch = [tlist objectAtIndex:i]; - if (touch.phase != UITouchPhaseMoved) - continue; - int tid = get_touch_id(touch); - ERR_FAIL_COND(tid == -1); - CGPoint touchPoint = [touch locationInView:self]; - CGPoint prev_point = [touch previousLocationInView:self]; - OSIPhone::get_singleton()->touch_drag(tid, prev_point.x * self.contentScaleFactor, prev_point.y * self.contentScaleFactor, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor); - }; - }; -} - -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - NSArray *tlist = [[event allTouches] allObjects]; - for (unsigned int i = 0; i < [tlist count]; i++) { - if ([touches containsObject:[tlist objectAtIndex:i]]) { - UITouch *touch = [tlist objectAtIndex:i]; - if (touch.phase != UITouchPhaseEnded) - continue; - int tid = get_touch_id(touch); - ERR_FAIL_COND(tid == -1); - remove_touch(touch); - CGPoint touchPoint = [touch locationInView:self]; - OSIPhone::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, false, false); - }; - }; -} - -- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { - OSIPhone::get_singleton()->touches_cancelled(); - clear_touches(); -}; - -- (BOOL)canBecomeFirstResponder { - return YES; -}; - -- (void)open_keyboard { - //keyboard_text = p_existing; - [self becomeFirstResponder]; -}; - -- (void)hide_keyboard { - //keyboard_text = p_existing; - [self resignFirstResponder]; -}; - -- (void)keyboardOnScreen:(NSNotification *)notification { - NSDictionary *info = notification.userInfo; - NSValue *value = info[UIKeyboardFrameEndUserInfoKey]; - - CGRect rawFrame = [value CGRectValue]; - CGRect keyboardFrame = [self convertRect:rawFrame fromView:nil]; - - OSIPhone::get_singleton()->set_virtual_keyboard_height(_points_to_pixels(keyboardFrame.size.height)); -} - -- (void)keyboardHidden:(NSNotification *)notification { - OSIPhone::get_singleton()->set_virtual_keyboard_height(0); -} - -- (void)deleteBackward { - if (keyboard_text.length()) - keyboard_text.erase(keyboard_text.length() - 1, 1); - OSIPhone::get_singleton()->key(KEY_BACKSPACE, true); -}; - -- (BOOL)hasText { - return keyboard_text.length() ? YES : NO; -}; - -- (void)insertText:(NSString *)p_text { - String character; - character.parse_utf8([p_text UTF8String]); - keyboard_text = keyboard_text + character; - OSIPhone::get_singleton()->key(character[0] == 10 ? KEY_ENTER : character[0], true); - printf("inserting text with character %lc\n", (CharType)character[0]); -}; - -- (void)audioRouteChangeListenerCallback:(NSNotification *)notification { - printf("*********** route changed!\n"); - NSDictionary *interuptionDict = notification.userInfo; - - NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue]; - - switch (routeChangeReason) { - case AVAudioSessionRouteChangeReasonNewDeviceAvailable: { - NSLog(@"AVAudioSessionRouteChangeReasonNewDeviceAvailable"); - NSLog(@"Headphone/Line plugged in"); - }; break; - - case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: { - NSLog(@"AVAudioSessionRouteChangeReasonOldDeviceUnavailable"); - NSLog(@"Headphone/Line was pulled. Resuming video play...."); - if (_is_video_playing()) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [_instance.avPlayer play]; // NOTE: change this line according your current player implementation - NSLog(@"resumed play"); - }); - }; - }; break; - - case AVAudioSessionRouteChangeReasonCategoryChange: { - // called at start - also when other audio wants to play - NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange"); - }; break; - } -} - -// When created via code however, we get initWithFrame -- (id)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - _instance = self; - printf("after init super %p\n", self); - if (self != nil) { - self = [self initGLES]; - printf("after init gles %p\n", self); - } - init_touches(); - self.multipleTouchEnabled = YES; - self.autocorrectionType = UITextAutocorrectionTypeNo; - - printf("******** adding observer for sound routing changes\n"); - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(audioRouteChangeListenerCallback:) - name:AVAudioSessionRouteChangeNotification - object:nil]; - - printf("******** adding observer for keyboard show/hide\n"); - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(keyboardOnScreen:) - name:UIKeyboardDidShowNotification - object:nil]; - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(keyboardHidden:) - name:UIKeyboardDidHideNotification - object:nil]; - - //self.autoresizesSubviews = YES; - //[self setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleWidth]; - - return self; -} - -//- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers { -// return YES; -//} - -//- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{ -// return YES; -//} - -// Stop animating and release resources when they are no longer needed. -- (void)dealloc { - [self stopAnimation]; - - if ([EAGLContext currentContext] == context) { - [EAGLContext setCurrentContext:nil]; - } - - [context release]; - context = nil; - - [super dealloc]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (object == _instance.avPlayerItem && [keyPath isEqualToString:@"status"]) { - if (_instance.avPlayerItem.status == AVPlayerStatusFailed || _instance.avPlayer.status == AVPlayerStatusFailed) { - _stop_video(); - video_found_error = true; - } - - if (_instance.avPlayer.status == AVPlayerStatusReadyToPlay && - _instance.avPlayerItem.status == AVPlayerItemStatusReadyToPlay && - CMTIME_COMPARE_INLINE(video_current_time, ==, kCMTimeZero)) { - //NSLog(@"time: %@", video_current_time); - - [_instance.avPlayer seekToTime:video_current_time]; - video_current_time = kCMTimeZero; - } - } - - if (object == _instance.avPlayer && [keyPath isEqualToString:@"rate"]) { - NSLog(@"Player playback rate changed: %.5f", _instance.avPlayer.rate); - if (_is_video_playing() && _instance.avPlayer.rate == 0.0 && !_instance.avPlayer.error) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [_instance.avPlayer play]; // NOTE: change this line according your current player implementation - NSLog(@"resumed play"); - }); - - NSLog(@" . . . PAUSED (or just started)"); - } - } -} - -- (void)playerItemDidReachEnd:(NSNotification *)notification { - _stop_video(); -} - -@end diff --git a/platform/iphone/godot_iphone.cpp b/platform/iphone/godot_iphone.mm index b9d217c9d2..6d95276f37 100644 --- a/platform/iphone/godot_iphone.cpp +++ b/platform/iphone/godot_iphone.mm @@ -1,5 +1,5 @@ /*************************************************************************/ -/* godot_iphone.cpp */ +/* godot_iphone.mm */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -38,19 +38,49 @@ static OSIPhone *os = nullptr; -extern "C" { -int add_path(int p_argc, char **p_args); -int add_cmdline(int p_argc, char **p_args); +int add_path(int, char **); +int add_cmdline(int, char **); +int iphone_main(int, char **, String); + +int add_path(int p_argc, char **p_args) { + NSString *str = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_path"]; + if (!str) { + return p_argc; + } + + p_args[p_argc++] = (char *)"--path"; + p_args[p_argc++] = (char *)[str cStringUsingEncoding:NSUTF8StringEncoding]; + p_args[p_argc] = NULL; + + return p_argc; }; -int iphone_main(int, int, int, char **, String); +int add_cmdline(int p_argc, char **p_args) { + NSArray *arr = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_cmdline"]; + if (!arr) { + return p_argc; + } + + for (NSUInteger i = 0; i < [arr count]; i++) { + NSString *str = [arr objectAtIndex:i]; + if (!str) { + continue; + } + p_args[p_argc++] = (char *)[str cStringUsingEncoding:NSUTF8StringEncoding]; + }; + + p_args[p_argc] = NULL; + + return p_argc; +}; -int iphone_main(int width, int height, int argc, char **argv, String data_dir) { +int iphone_main(int argc, char **argv, String data_dir) { size_t len = strlen(argv[0]); while (len--) { - if (argv[0][len] == '/') + if (argv[0][len] == '/') { break; + } } if (len >= 0) { @@ -65,7 +95,10 @@ int iphone_main(int width, int height, int argc, char **argv, String data_dir) { char cwd[512]; getcwd(cwd, sizeof(cwd)); printf("cwd %s\n", cwd); - os = new OSIPhone(width, height, data_dir); + os = new OSIPhone(data_dir); + + // We must override main when testing is enabled + TEST_MAIN_OVERRIDE char *fargv[64]; for (int i = 0; i < argc; i++) { @@ -76,10 +109,14 @@ int iphone_main(int width, int height, int argc, char **argv, String data_dir) { argc = add_cmdline(argc, fargv); printf("os created\n"); + Error err = Main::setup(fargv[0], argc - 1, &fargv[1], false); printf("setup %i\n", err); - if (err != OK) + if (err != OK) { return 255; + } + + os->initialize_modules(); return 0; }; diff --git a/platform/iphone/godot_view.h b/platform/iphone/godot_view.h new file mode 100644 index 0000000000..62fa2f5a32 --- /dev/null +++ b/platform/iphone/godot_view.h @@ -0,0 +1,56 @@ +/*************************************************************************/ +/* godot_view.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 <UIKit/UIKit.h> + +class String; + +@protocol DisplayLayer; +@protocol GodotViewRendererProtocol; + +@interface GodotView : UIView <UIKeyInput> + +@property(assign, nonatomic) id<GodotViewRendererProtocol> renderer; + +@property(assign, readonly, nonatomic) BOOL isActive; + +@property(assign, nonatomic) BOOL useCADisplayLink; +@property(strong, readonly, nonatomic) CALayer<DisplayLayer> *renderingLayer; +@property(assign, readonly, nonatomic) BOOL canRender; + +@property(assign, nonatomic) NSTimeInterval renderingInterval; + +- (CALayer<DisplayLayer> *)initializeRenderingForDriver:(NSString *)driverName; +- (void)stopRendering; +- (void)startRendering; + +- (BOOL)becomeFirstResponderWithString:(String)p_existing; + +@end diff --git a/platform/iphone/godot_view.mm b/platform/iphone/godot_view.mm new file mode 100644 index 0000000000..3b4344c46d --- /dev/null +++ b/platform/iphone/godot_view.mm @@ -0,0 +1,493 @@ +/*************************************************************************/ +/* godot_view.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "godot_view.h" +#include "core/os/keyboard.h" +#include "core/ustring.h" +#import "display_layer.h" +#include "display_server_iphone.h" +#import "godot_view_gesture_recognizer.h" +#import "godot_view_renderer.h" + +#import <CoreMotion/CoreMotion.h> + +static const int max_touches = 8; + +@interface GodotView () { + UITouch *godot_touches[max_touches]; + String keyboard_text; +} + +@property(assign, nonatomic) BOOL isActive; + +// CADisplayLink available on 3.1+ synchronizes the animation timer & drawing with the refresh rate of the display, only supports animation intervals of 1/60 1/30 & 1/15 +@property(strong, nonatomic) CADisplayLink *displayLink; + +// An animation timer that, when animation is started, will periodically call -drawView at the given rate. +// Only used if CADisplayLink is not +@property(strong, nonatomic) NSTimer *animationTimer; + +@property(strong, nonatomic) CALayer<DisplayLayer> *renderingLayer; + +@property(strong, nonatomic) CMMotionManager *motionManager; + +@property(strong, nonatomic) GodotViewGestureRecognizer *delayGestureRecognizer; + +@end + +@implementation GodotView + +- (CALayer<DisplayLayer> *)initializeRenderingForDriver:(NSString *)driverName { + if (self.renderingLayer) { + return self.renderingLayer; + } + + CALayer<DisplayLayer> *layer; + + if ([driverName isEqualToString:@"vulkan"]) { + layer = [GodotMetalLayer layer]; + } else if ([driverName isEqualToString:@"opengl_es"]) { + if (@available(iOS 13, *)) { + NSLog(@"OpenGL ES is deprecated on iOS 13"); + } +#if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR + return nil; +#else + layer = [GodotOpenGLLayer layer]; +#endif + } else { + return nil; + } + + layer.frame = self.bounds; + layer.contentsScale = self.contentScaleFactor; + + [self.layer addSublayer:layer]; + self.renderingLayer = layer; + + [layer initializeDisplayLayer]; + + return self.renderingLayer; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)dealloc { + [self stopRendering]; + + self.renderer = nil; + + if (self.renderingLayer) { + [self.renderingLayer removeFromSuperlayer]; + self.renderingLayer = nil; + } + + if (self.motionManager) { + [self.motionManager stopDeviceMotionUpdates]; + self.motionManager = nil; + } + + if (self.displayLink) { + [self.displayLink invalidate]; + self.displayLink = nil; + } + + if (self.animationTimer) { + [self.animationTimer invalidate]; + self.animationTimer = nil; + } + + if (self.delayGestureRecognizer) { + self.delayGestureRecognizer = nil; + } +} + +- (void)godot_commonInit { + self.contentScaleFactor = [UIScreen mainScreen].nativeScale; + + [self initTouches]; + + // Configure and start accelerometer + if (!self.motionManager) { + self.motionManager = [[CMMotionManager alloc] init]; + if (self.motionManager.deviceMotionAvailable) { + self.motionManager.deviceMotionUpdateInterval = 1.0 / 70.0; + [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXMagneticNorthZVertical]; + } else { + self.motionManager = nil; + } + } + + // Initialize delay gesture recognizer + GodotViewGestureRecognizer *gestureRecognizer = [[GodotViewGestureRecognizer alloc] init]; + self.delayGestureRecognizer = gestureRecognizer; + [self addGestureRecognizer:self.delayGestureRecognizer]; +} + +- (void)stopRendering { + if (!self.isActive) { + return; + } + + self.isActive = NO; + + printf("******** stop animation!\n"); + + if (self.useCADisplayLink) { + [self.displayLink invalidate]; + self.displayLink = nil; + } else { + [self.animationTimer invalidate]; + self.animationTimer = nil; + } + + [self clearTouches]; +} + +- (void)startRendering { + if (self.isActive) { + return; + } + + self.isActive = YES; + + printf("start animation!\n"); + + if (self.useCADisplayLink) { + self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView)]; + + // Approximate frame rate + // assumes device refreshes at 60 fps + int displayFPS = (NSInteger)(1.0 / self.renderingInterval); + + self.displayLink.preferredFramesPerSecond = displayFPS; + + // Setup DisplayLink in main thread + [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + } else { + self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:self.renderingInterval target:self selector:@selector(drawView) userInfo:nil repeats:YES]; + } +} + +- (void)drawView { + if (!self.isActive) { + printf("draw view not active!\n"); + return; + } + + if (self.useCADisplayLink) { + // Pause the CADisplayLink to avoid recursion + [self.displayLink setPaused:YES]; + + // Process all input events + while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource) + ; + + // We are good to go, resume the CADisplayLink + [self.displayLink setPaused:NO]; + } + + [self.renderingLayer renderDisplayLayer]; + + if (!self.renderer) { + return; + } + + if ([self.renderer setupView:self]) { + return; + } + + [self handleMotion]; + [self.renderer renderOnView:self]; +} + +- (BOOL)canRender { + if (self.useCADisplayLink) { + return self.displayLink != nil; + } else { + return self.animationTimer != nil; + } +} + +- (void)setRenderingInterval:(NSTimeInterval)renderingInterval { + _renderingInterval = renderingInterval; + + if (self.canRender) { + [self stopRendering]; + [self startRendering]; + } +} + +- (void)layoutSubviews { + if (self.renderingLayer) { + self.renderingLayer.frame = self.bounds; + [self.renderingLayer layoutDisplayLayer]; + + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->resize_window(self.bounds.size); + } + } + + [super layoutSubviews]; +} + +// MARK: - Input + +// MARK: Keyboard + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (BOOL)becomeFirstResponderWithString:(String)p_existing { + keyboard_text = p_existing; + return [self becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder { + keyboard_text = String(); + return [super resignFirstResponder]; +} + +- (void)deleteBackward { + if (keyboard_text.length()) { + keyboard_text.erase(keyboard_text.length() - 1, 1); + } + DisplayServerIPhone::get_singleton()->key(KEY_BACKSPACE, true); +} + +- (BOOL)hasText { + return keyboard_text.length() > 0; +} + +- (void)insertText:(NSString *)p_text { + String character; + character.parse_utf8([p_text UTF8String]); + keyboard_text = keyboard_text + character; + DisplayServerIPhone::get_singleton()->key(character[0] == 10 ? KEY_ENTER : character[0], true); +} + +// MARK: Touches + +- (void)initTouches { + for (int i = 0; i < max_touches; i++) { + godot_touches[i] = NULL; + } +} + +- (int)getTouchIDForTouch:(UITouch *)p_touch { + int first = -1; + for (int i = 0; i < max_touches; i++) { + if (first == -1 && godot_touches[i] == NULL) { + first = i; + continue; + } + if (godot_touches[i] == p_touch) { + return i; + } + } + + if (first != -1) { + godot_touches[first] = p_touch; + return first; + } + + return -1; +} + +- (int)removeTouch:(UITouch *)p_touch { + int remaining = 0; + for (int i = 0; i < max_touches; i++) { + if (godot_touches[i] == NULL) { + continue; + } + if (godot_touches[i] == p_touch) { + godot_touches[i] = NULL; + } else { + ++remaining; + } + } + return remaining; +} + +- (void)clearTouches { + for (int i = 0; i < max_touches; i++) { + godot_touches[i] = NULL; + } +} + +- (void)touchesBegan:(NSSet *)touchesSet withEvent:(UIEvent *)event { + NSArray *tlist = [event.allTouches allObjects]; + for (unsigned int i = 0; i < [tlist count]; i++) { + if ([touchesSet containsObject:[tlist objectAtIndex:i]]) { + UITouch *touch = [tlist objectAtIndex:i]; + int tid = [self getTouchIDForTouch:touch]; + ERR_FAIL_COND(tid == -1); + CGPoint touchPoint = [touch locationInView:self]; + DisplayServerIPhone::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, true, touch.tapCount > 1); + } + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + NSArray *tlist = [event.allTouches allObjects]; + for (unsigned int i = 0; i < [tlist count]; i++) { + if ([touches containsObject:[tlist objectAtIndex:i]]) { + UITouch *touch = [tlist objectAtIndex:i]; + int tid = [self getTouchIDForTouch:touch]; + ERR_FAIL_COND(tid == -1); + CGPoint touchPoint = [touch locationInView:self]; + CGPoint prev_point = [touch previousLocationInView:self]; + DisplayServerIPhone::get_singleton()->touch_drag(tid, prev_point.x * self.contentScaleFactor, prev_point.y * self.contentScaleFactor, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor); + } + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + NSArray *tlist = [event.allTouches allObjects]; + for (unsigned int i = 0; i < [tlist count]; i++) { + if ([touches containsObject:[tlist objectAtIndex:i]]) { + UITouch *touch = [tlist objectAtIndex:i]; + int tid = [self getTouchIDForTouch:touch]; + ERR_FAIL_COND(tid == -1); + [self removeTouch:touch]; + CGPoint touchPoint = [touch locationInView:self]; + DisplayServerIPhone::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, false, false); + } + } +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + NSArray *tlist = [event.allTouches allObjects]; + for (unsigned int i = 0; i < [tlist count]; i++) { + if ([touches containsObject:[tlist objectAtIndex:i]]) { + UITouch *touch = [tlist objectAtIndex:i]; + int tid = [self getTouchIDForTouch:touch]; + ERR_FAIL_COND(tid == -1); + DisplayServerIPhone::get_singleton()->touches_cancelled(tid); + } + } + [self clearTouches]; +} + +// MARK: Motion + +- (void)handleMotion { + if (!self.motionManager) { + return; + } + + // Just using polling approach for now, we can set this up so it sends + // data to us in intervals, might be better. See Apple reference pages + // for more details: + // https://developer.apple.com/reference/coremotion/cmmotionmanager?language=objc + + // Apple splits our accelerometer date into a gravity and user movement + // component. We add them back together + CMAcceleration gravity = self.motionManager.deviceMotion.gravity; + CMAcceleration acceleration = self.motionManager.deviceMotion.userAcceleration; + + ///@TODO We don't seem to be getting data here, is my device broken or + /// is this code incorrect? + CMMagneticField magnetic = self.motionManager.deviceMotion.magneticField.field; + + ///@TODO we can access rotationRate as a CMRotationRate variable + ///(processed date) or CMGyroData (raw data), have to see what works + /// best + CMRotationRate rotation = self.motionManager.deviceMotion.rotationRate; + + // Adjust for screen orientation. + // [[UIDevice currentDevice] orientation] changes even if we've fixed + // our orientation which is not a good thing when you're trying to get + // your user to move the screen in all directions and want consistent + // output + + ///@TODO Using [[UIApplication sharedApplication] statusBarOrientation] + /// is a bit of a hack. Godot obviously knows the orientation so maybe + /// we + // can use that instead? (note that left and right seem swapped) + + UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown; + + if (@available(iOS 13, *)) { + interfaceOrientation = [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation; +#if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR + } else { + interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation]; +#endif + } + + switch (interfaceOrientation) { + case UIInterfaceOrientationLandscapeLeft: { + DisplayServerIPhone::get_singleton()->update_gravity(-gravity.y, gravity.x, gravity.z); + DisplayServerIPhone::get_singleton()->update_accelerometer(-(acceleration.y + gravity.y), (acceleration.x + gravity.x), acceleration.z + gravity.z); + DisplayServerIPhone::get_singleton()->update_magnetometer(-magnetic.y, magnetic.x, magnetic.z); + DisplayServerIPhone::get_singleton()->update_gyroscope(-rotation.y, rotation.x, rotation.z); + } break; + case UIInterfaceOrientationLandscapeRight: { + DisplayServerIPhone::get_singleton()->update_gravity(gravity.y, -gravity.x, gravity.z); + DisplayServerIPhone::get_singleton()->update_accelerometer((acceleration.y + gravity.y), -(acceleration.x + gravity.x), acceleration.z + gravity.z); + DisplayServerIPhone::get_singleton()->update_magnetometer(magnetic.y, -magnetic.x, magnetic.z); + DisplayServerIPhone::get_singleton()->update_gyroscope(rotation.y, -rotation.x, rotation.z); + } break; + case UIInterfaceOrientationPortraitUpsideDown: { + DisplayServerIPhone::get_singleton()->update_gravity(-gravity.x, gravity.y, gravity.z); + DisplayServerIPhone::get_singleton()->update_accelerometer(-(acceleration.x + gravity.x), (acceleration.y + gravity.y), acceleration.z + gravity.z); + DisplayServerIPhone::get_singleton()->update_magnetometer(-magnetic.x, magnetic.y, magnetic.z); + DisplayServerIPhone::get_singleton()->update_gyroscope(-rotation.x, rotation.y, rotation.z); + } break; + default: { // assume portrait + DisplayServerIPhone::get_singleton()->update_gravity(gravity.x, gravity.y, gravity.z); + DisplayServerIPhone::get_singleton()->update_accelerometer(acceleration.x + gravity.x, acceleration.y + gravity.y, acceleration.z + gravity.z); + DisplayServerIPhone::get_singleton()->update_magnetometer(magnetic.x, magnetic.y, magnetic.z); + DisplayServerIPhone::get_singleton()->update_gyroscope(rotation.x, rotation.y, rotation.z); + } break; + } +} + +@end diff --git a/platform/iphone/godot_view_gesture_recognizer.h b/platform/iphone/godot_view_gesture_recognizer.h new file mode 100644 index 0000000000..1431a9fb89 --- /dev/null +++ b/platform/iphone/godot_view_gesture_recognizer.h @@ -0,0 +1,46 @@ +/*************************************************************************/ +/* godot_view_gesture_recognizer.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +// GLViewGestureRecognizer allows iOS gestures to work currectly by +// emulating UIScrollView's UIScrollViewDelayedTouchesBeganGestureRecognizer. +// It catches all gestures incoming to UIView and delays them for 150ms +// (the same value used by UIScrollViewDelayedTouchesBeganGestureRecognizer) +// If touch cancellation or end message is fired it fires delayed +// begin touch immediately as well as last touch signal + +#import <UIKit/UIKit.h> + +@interface GodotViewGestureRecognizer : UIGestureRecognizer + +@property(nonatomic, readonly, assign) NSTimeInterval delayTimeInterval; + +- (instancetype)init; + +@end diff --git a/platform/iphone/godot_view_gesture_recognizer.mm b/platform/iphone/godot_view_gesture_recognizer.mm new file mode 100644 index 0000000000..71367a629c --- /dev/null +++ b/platform/iphone/godot_view_gesture_recognizer.mm @@ -0,0 +1,171 @@ +/*************************************************************************/ +/* godot_view_gesture_recognizer.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "godot_view_gesture_recognizer.h" + +#include "core/project_settings.h" + +// Minimum distance for touches to move to fire +// a delay timer before scheduled time. +// Should be the low enough to not cause issues with dragging +// but big enough to allow click to work. +const CGFloat kGLGestureMovementDistance = 0.5; + +@interface GodotViewGestureRecognizer () + +@property(nonatomic, readwrite, assign) NSTimeInterval delayTimeInterval; + +@end + +@interface GodotViewGestureRecognizer () + +// Timer used to delay begin touch message. +// Should work as simple emulation of UIDelayedAction +@property(strong, nonatomic) NSTimer *delayTimer; + +// Delayed touch parameters +@property(strong, nonatomic) NSSet *delayedTouches; +@property(strong, nonatomic) UIEvent *delayedEvent; + +@end + +@implementation GodotViewGestureRecognizer + +- (instancetype)init { + self = [super init]; + + self.cancelsTouchesInView = YES; + self.delaysTouchesBegan = YES; + self.delaysTouchesEnded = YES; + + self.delayTimeInterval = GLOBAL_GET("input_devices/pointing/ios/touch_delay"); + + return self; +} + +- (void)dealloc { + if (self.delayTimer) { + [self.delayTimer invalidate]; + self.delayTimer = nil; + } + + if (self.delayedTouches) { + self.delayedTouches = nil; + } + + if (self.delayedEvent) { + self.delayedEvent = nil; + } +} + +- (void)delayTouches:(NSSet *)touches andEvent:(UIEvent *)event { + [self.delayTimer fire]; + + self.delayedTouches = touches; + self.delayedEvent = event; + + self.delayTimer = [NSTimer + scheduledTimerWithTimeInterval:self.delayTimeInterval + target:self + selector:@selector(fireDelayedTouches:) + userInfo:nil + repeats:NO]; +} + +- (void)fireDelayedTouches:(id)timer { + [self.delayTimer invalidate]; + self.delayTimer = nil; + + if (self.delayedTouches) { + [self.view touchesBegan:self.delayedTouches withEvent:self.delayedEvent]; + } + + self.delayedTouches = nil; + self.delayedEvent = nil; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseBegan]; + [self delayTouches:cleared andEvent:event]; +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseMoved]; + + if (self.delayTimer) { + // We should check if movement was significant enough to fire an event + // for dragging to work correctly. + for (UITouch *touch in cleared) { + CGPoint from = [touch locationInView:self.view]; + CGPoint to = [touch previousLocationInView:self.view]; + CGFloat xDistance = from.x - to.x; + CGFloat yDistance = from.y - to.y; + + CGFloat distance = sqrt(xDistance * xDistance + yDistance * yDistance); + + // Early exit, since one of touches has moved enough to fire a drag event. + if (distance > kGLGestureMovementDistance) { + [self.delayTimer fire]; + [self.view touchesMoved:cleared withEvent:event]; + return; + } + } + + return; + } + + [self.view touchesMoved:cleared withEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + [self.delayTimer fire]; + + NSSet *cleared = [self copyClearedTouches:touches phase:UITouchPhaseEnded]; + [self.view touchesEnded:cleared withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + [self.delayTimer fire]; + [self.view touchesCancelled:touches withEvent:event]; +}; + +- (NSSet *)copyClearedTouches:(NSSet *)touches phase:(UITouchPhase)phaseToSave { + NSMutableSet *cleared = [touches mutableCopy]; + + for (UITouch *touch in touches) { + if (touch.phase != phaseToSave) { + [cleared removeObject:touch]; + } + } + + return cleared; +} + +@end diff --git a/platform/iphone/godot_view_renderer.h b/platform/iphone/godot_view_renderer.h new file mode 100644 index 0000000000..ea8998c808 --- /dev/null +++ b/platform/iphone/godot_view_renderer.h @@ -0,0 +1,44 @@ +/*************************************************************************/ +/* godot_view_renderer.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 <UIKit/UIKit.h> + +@protocol GodotViewRendererProtocol <NSObject> + +@property(assign, readonly, nonatomic) BOOL hasFinishedSetup; + +- (BOOL)setupView:(UIView *)view; +- (void)renderOnView:(UIView *)view; + +@end + +@interface GodotViewRenderer : NSObject <GodotViewRendererProtocol> + +@end diff --git a/platform/iphone/godot_view_renderer.mm b/platform/iphone/godot_view_renderer.mm new file mode 100644 index 0000000000..045fb451f0 --- /dev/null +++ b/platform/iphone/godot_view_renderer.mm @@ -0,0 +1,117 @@ +/*************************************************************************/ +/* godot_view_renderer.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "godot_view_renderer.h" +#include "core/os/keyboard.h" +#include "core/project_settings.h" +#import "display_server_iphone.h" +#include "main/main.h" +#include "os_iphone.h" +#include "servers/audio_server.h" + +#import <AudioToolbox/AudioServices.h> +#import <CoreMotion/CoreMotion.h> +#import <GameController/GameController.h> +#import <QuartzCore/QuartzCore.h> +#import <UIKit/UIKit.h> + +@interface GodotViewRenderer () + +@property(assign, nonatomic) BOOL hasFinishedProjectDataSetup; +@property(assign, nonatomic) BOOL hasStartedMain; +@property(assign, nonatomic) BOOL hasFinishedSetup; + +@end + +@implementation GodotViewRenderer + +- (BOOL)setupView:(UIView *)view { + if (self.hasFinishedSetup) { + return NO; + } + + if (!OS::get_singleton()) { + exit(0); + } + + if (!self.hasFinishedProjectDataSetup) { + [self setupProjectData]; + return YES; + } + + if (!self.hasStartedMain) { + self.hasStartedMain = YES; + OSIPhone::get_singleton()->start(); + return YES; + } + + self.hasFinishedSetup = YES; + + return NO; +} + +- (void)setupProjectData { + self.hasFinishedProjectDataSetup = YES; + + Main::setup2(); + + // this might be necessary before here + NSDictionary *dict = [[NSBundle mainBundle] infoDictionary]; + for (NSString *key in dict) { + NSObject *value = [dict objectForKey:key]; + String ukey = String::utf8([key UTF8String]); + + // we need a NSObject to Variant conversor + + if ([value isKindOfClass:[NSString class]]) { + NSString *str = (NSString *)value; + String uval = String::utf8([str UTF8String]); + + ProjectSettings::get_singleton()->set("Info.plist/" + ukey, uval); + + } else if ([value isKindOfClass:[NSNumber class]]) { + NSNumber *n = (NSNumber *)value; + double dval = [n doubleValue]; + + ProjectSettings::get_singleton()->set("Info.plist/" + ukey, dval); + }; + // do stuff + } +} + +- (void)renderOnView:(UIView *)view { + if (!OSIPhone::get_singleton()) { + return; + } + + OSIPhone::get_singleton()->iterate(); +} + +@end diff --git a/platform/iphone/icloud.h b/platform/iphone/icloud.h index b11e22fec6..6ca1e6594a 100644 --- a/platform/iphone/icloud.h +++ b/platform/iphone/icloud.h @@ -33,7 +33,7 @@ #ifndef ICLOUD_H #define ICLOUD_H -#include "core/object.h" +#include "core/class_db.h" class ICloud : public Object { GDCLASS(ICloud, Object); @@ -44,9 +44,9 @@ class ICloud : public Object { List<Variant> pending_events; public: - Error remove_key(Variant p_param); - Variant set_key_values(Variant p_param); - Variant get_key_value(Variant p_param); + Error remove_key(String p_param); + Array set_key_values(Dictionary p_params); + Variant get_key_value(String p_param); Error synchronize_key_values(); Variant get_all_key_values(); diff --git a/platform/iphone/icloud.mm b/platform/iphone/icloud.mm index c768274b1f..3d81349883 100644 --- a/platform/iphone/icloud.mm +++ b/platform/iphone/icloud.mm @@ -48,8 +48,10 @@ ICloud *ICloud::instance = NULL; void ICloud::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_key"), &ICloud::remove_key); + ClassDB::bind_method(D_METHOD("set_key_values"), &ICloud::set_key_values); ClassDB::bind_method(D_METHOD("get_key_value"), &ICloud::get_key_value); + ClassDB::bind_method(D_METHOD("synchronize_key_values"), &ICloud::synchronize_key_values); ClassDB::bind_method(D_METHOD("get_all_key_values"), &ICloud::get_all_key_values); @@ -91,7 +93,7 @@ Variant nsobject_to_variant(NSObject *object) { } else if ([object isKindOfClass:[NSArray class]]) { Array result; NSArray *array = (NSArray *)object; - for (unsigned int i = 0; i < [array count]; ++i) { + for (NSUInteger i = 0; i < [array count]; ++i) { NSObject *value = [array objectAtIndex:i]; result.push_back(nsobject_to_variant(value)); } @@ -148,19 +150,19 @@ Variant nsobject_to_variant(NSObject *object) { NSObject *variant_to_nsobject(Variant v) { if (v.get_type() == Variant::STRING) { - return [[[NSString alloc] initWithUTF8String:((String)v).utf8().get_data()] autorelease]; - } else if (v.get_type() == Variant::REAL) { + return [[NSString alloc] initWithUTF8String:((String)v).utf8().get_data()]; + } else if (v.get_type() == Variant::FLOAT) { return [NSNumber numberWithDouble:(double)v]; } else if (v.get_type() == Variant::INT) { return [NSNumber numberWithLongLong:(long)(int)v]; } else if (v.get_type() == Variant::BOOL) { return [NSNumber numberWithBool:BOOL((bool)v)]; } else if (v.get_type() == Variant::DICTIONARY) { - NSMutableDictionary *result = [[[NSMutableDictionary alloc] init] autorelease]; + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; Dictionary dic = v; Array keys = dic.keys(); - for (unsigned int i = 0; i < keys.size(); ++i) { - NSString *key = [[[NSString alloc] initWithUTF8String:((String)(keys[i])).utf8().get_data()] autorelease]; + for (int i = 0; i < keys.size(); ++i) { + NSString *key = [[NSString alloc] initWithUTF8String:((String)(keys[i])).utf8().get_data()]; NSObject *value = variant_to_nsobject(dic[keys[i]]); if (key == NULL || value == NULL) { @@ -171,9 +173,9 @@ NSObject *variant_to_nsobject(Variant v) { } return result; } else if (v.get_type() == Variant::ARRAY) { - NSMutableArray *result = [[[NSMutableArray alloc] init] autorelease]; + NSMutableArray *result = [[NSMutableArray alloc] init]; Array arr = v; - for (unsigned int i = 0; i < arr.size(); ++i) { + for (int i = 0; i < arr.size(); ++i) { NSObject *value = variant_to_nsobject(arr[i]); if (value == NULL) { //trying to add something unsupported to the array. cancel the whole array @@ -192,9 +194,8 @@ NSObject *variant_to_nsobject(Variant v) { return NULL; } -Error ICloud::remove_key(Variant p_param) { - String param = p_param; - NSString *key = [[[NSString alloc] initWithUTF8String:param.utf8().get_data()] autorelease]; +Error ICloud::remove_key(String p_param) { + NSString *key = [[NSString alloc] initWithUTF8String:p_param.utf8().get_data()]; NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; @@ -207,17 +208,16 @@ Error ICloud::remove_key(Variant p_param) { } //return an array of the keys that could not be set -Variant ICloud::set_key_values(Variant p_params) { - Dictionary params = p_params; - Array keys = params.keys(); +Array ICloud::set_key_values(Dictionary p_params) { + Array keys = p_params.keys(); Array error_keys; - for (unsigned int i = 0; i < keys.size(); ++i) { + for (int i = 0; i < keys.size(); ++i) { String variant_key = keys[i]; - Variant variant_value = params[variant_key]; + Variant variant_value = p_params[variant_key]; - NSString *key = [[[NSString alloc] initWithUTF8String:variant_key.utf8().get_data()] autorelease]; + NSString *key = [[NSString alloc] initWithUTF8String:variant_key.utf8().get_data()]; if (key == NULL) { error_keys.push_back(variant_key); continue; @@ -237,10 +237,8 @@ Variant ICloud::set_key_values(Variant p_params) { return error_keys; } -Variant ICloud::get_key_value(Variant p_param) { - String param = p_param; - - NSString *key = [[[NSString alloc] initWithUTF8String:param.utf8().get_data()] autorelease]; +Variant ICloud::get_key_value(String p_param) { + NSString *key = [[NSString alloc] initWithUTF8String:p_param.utf8().get_data()]; NSUbiquitousKeyValueStore *store = [NSUbiquitousKeyValueStore defaultStore]; if (![[store dictionaryRepresentation] objectForKey:key]) { diff --git a/platform/iphone/in_app_store.h b/platform/iphone/in_app_store.h index 44e65e77ed..1b8f84ec7b 100644 --- a/platform/iphone/in_app_store.h +++ b/platform/iphone/in_app_store.h @@ -33,7 +33,18 @@ #ifndef IN_APP_STORE_H #define IN_APP_STORE_H -#include "core/object.h" +#include "core/class_db.h" + +#ifdef __OBJC__ +@class GodotProductsDelegate; +@class GodotTransactionsObserver; + +typedef GodotProductsDelegate InAppStoreProductDelegate; +typedef GodotTransactionsObserver InAppStoreTransactionObserver; +#else +typedef void InAppStoreProductDelegate; +typedef void InAppStoreTransactionObserver; +#endif class InAppStore : public Object { GDCLASS(InAppStore, Object); @@ -43,10 +54,13 @@ class InAppStore : public Object { List<Variant> pending_events; + InAppStoreProductDelegate *products_request_delegate; + InAppStoreTransactionObserver *transactions_observer; + public: - Error request_product_info(Variant p_params); + Error request_product_info(Dictionary p_params); Error restore_purchases(); - Error purchase(Variant p_params); + Error purchase(Dictionary p_params); int get_pending_event_count(); Variant pop_pending_event(); diff --git a/platform/iphone/in_app_store.mm b/platform/iphone/in_app_store.mm index 548dcc549d..3eec9dae69 100644 --- a/platform/iphone/in_app_store.mm +++ b/platform/iphone/in_app_store.mm @@ -32,56 +32,119 @@ #include "in_app_store.h" -extern "C" { #import <Foundation/Foundation.h> #import <StoreKit/StoreKit.h> -}; -bool auto_finish_transactions = true; -NSMutableDictionary *pending_transactions = [NSMutableDictionary dictionary]; +InAppStore *InAppStore::instance = NULL; @interface SKProduct (LocalizedPrice) + @property(nonatomic, readonly) NSString *localizedPrice; + @end //----------------------------------// // SKProduct extension //----------------------------------// @implementation SKProduct (LocalizedPrice) + - (NSString *)localizedPrice { NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; [numberFormatter setLocale:self.priceLocale]; NSString *formattedString = [numberFormatter stringFromNumber:self.price]; - [numberFormatter release]; return formattedString; } @end -InAppStore *InAppStore::instance = NULL; +@interface GodotProductsDelegate : NSObject <SKProductsRequestDelegate> -void InAppStore::_bind_methods() { - ClassDB::bind_method(D_METHOD("request_product_info"), &InAppStore::request_product_info); - ClassDB::bind_method(D_METHOD("restore_purchases"), &InAppStore::restore_purchases); - ClassDB::bind_method(D_METHOD("purchase"), &InAppStore::purchase); +@property(nonatomic, strong) NSMutableArray *loadedProducts; +@property(nonatomic, strong) NSMutableArray *pendingRequests; - ClassDB::bind_method(D_METHOD("get_pending_event_count"), &InAppStore::get_pending_event_count); - ClassDB::bind_method(D_METHOD("pop_pending_event"), &InAppStore::pop_pending_event); - ClassDB::bind_method(D_METHOD("finish_transaction"), &InAppStore::finish_transaction); - ClassDB::bind_method(D_METHOD("set_auto_finish_transaction"), &InAppStore::set_auto_finish_transaction); -}; - -@interface ProductsDelegate : NSObject <SKProductsRequestDelegate> { -}; +- (void)performRequestWithProductIDs:(NSSet *)productIDs; +- (Error)purchaseProductWithProductID:(NSString *)productID; +- (void)reset; @end -@implementation ProductsDelegate +@implementation GodotProductsDelegate + +- (instancetype)init { + self = [super init]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.loadedProducts = [NSMutableArray new]; + self.pendingRequests = [NSMutableArray new]; +} + +- (void)performRequestWithProductIDs:(NSSet *)productIDs { + SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:productIDs]; + + request.delegate = self; + [self.pendingRequests addObject:request]; + [request start]; +} + +- (Error)purchaseProductWithProductID:(NSString *)productID { + SKProduct *product = nil; + + NSLog(@"searching for product!"); + + if (self.loadedProducts) { + for (SKProduct *storedProduct in self.loadedProducts) { + if ([storedProduct.productIdentifier isEqualToString:productID]) { + product = storedProduct; + break; + } + } + } + + if (!product) { + return ERR_INVALID_PARAMETER; + } + + NSLog(@"product found!"); + + SKPayment *payment = [SKPayment paymentWithProduct:product]; + [[SKPaymentQueue defaultQueue] addPayment:payment]; + + NSLog(@"purchase sent!"); + + return OK; +} + +- (void)reset { + [self.loadedProducts removeAllObjects]; + [self.pendingRequests removeAllObjects]; +} + +- (void)request:(SKRequest *)request didFailWithError:(NSError *)error { + [self.pendingRequests removeObject:request]; + + Dictionary ret; + ret["type"] = "product_info"; + ret["result"] = "error"; + ret["error"] = String::utf8([error.localizedDescription UTF8String]); + + InAppStore::get_singleton()->_post_event(ret); +} - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { + [self.pendingRequests removeObject:request]; + NSArray *products = response.products; + [self.loadedProducts addObjectsFromArray:products]; + Dictionary ret; ret["type"] = "product_info"; ret["result"] = "ok"; @@ -104,7 +167,8 @@ void InAppStore::_bind_methods() { ids.push_back(String::utf8([product.productIdentifier UTF8String])); localized_prices.push_back(String::utf8([product.localizedPrice UTF8String])); currency_codes.push_back(String::utf8([[[product priceLocale] objectForKey:NSLocaleCurrencyCode] UTF8String])); - }; + } + ret["titles"] = titles; ret["descriptions"] = descriptions; ret["prices"] = prices; @@ -116,53 +180,54 @@ void InAppStore::_bind_methods() { for (NSString *ipid in response.invalidProductIdentifiers) { invalid_ids.push_back(String::utf8([ipid UTF8String])); - }; + } + ret["invalid_ids"] = invalid_ids; InAppStore::get_singleton()->_post_event(ret); - - [request release]; -}; +} @end -Error InAppStore::request_product_info(Variant p_params) { - Dictionary params = p_params; - ERR_FAIL_COND_V(!params.has("product_ids"), ERR_INVALID_PARAMETER); +@interface GodotTransactionsObserver : NSObject <SKPaymentTransactionObserver> - PackedStringArray pids = params["product_ids"]; - printf("************ request product info! %i\n", pids.size()); +@property(nonatomic, assign) BOOL shouldAutoFinishTransactions; +@property(nonatomic, strong) NSMutableDictionary *pendingTransactions; - NSMutableArray *array = [[[NSMutableArray alloc] initWithCapacity:pids.size()] autorelease]; - for (int i = 0; i < pids.size(); i++) { - printf("******** adding %ls to product list\n", pids[i].c_str()); - NSString *pid = [[[NSString alloc] initWithUTF8String:pids[i].utf8().get_data()] autorelease]; - [array addObject:pid]; - }; +- (void)finishTransactionWithProductID:(NSString *)productID; +- (void)reset; - NSSet *products = [[[NSSet alloc] initWithArray:array] autorelease]; - SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:products]; +@end - ProductsDelegate *delegate = [[ProductsDelegate alloc] init]; +@implementation GodotTransactionsObserver - request.delegate = delegate; - [request start]; +- (instancetype)init { + self = [super init]; - return OK; -}; + if (self) { + [self godot_commonInit]; + } -Error InAppStore::restore_purchases() { - printf("restoring purchases!\n"); - [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; + return self; +} - return OK; -}; +- (void)godot_commonInit { + self.pendingTransactions = [NSMutableDictionary new]; +} -@interface TransObserver : NSObject <SKPaymentTransactionObserver> { -}; -@end +- (void)finishTransactionWithProductID:(NSString *)productID { + SKPaymentTransaction *transaction = self.pendingTransactions[productID]; + + if (transaction) { + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + } -@implementation TransObserver + self.pendingTransactions[productID] = nil; +} + +- (void)reset { + [self.pendingTransactions removeAllObjects]; +} - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { printf("transactions updated!\n"); @@ -180,32 +245,17 @@ Error InAppStore::restore_purchases() { ret["transaction_id"] = transactionId; NSData *receipt = nil; - int sdk_version = 6; - - if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0) { - NSURL *receiptFileURL = nil; - NSBundle *bundle = [NSBundle mainBundle]; - if ([bundle respondsToSelector:@selector(appStoreReceiptURL)]) { - // Get the transaction receipt file path location in the app bundle. - receiptFileURL = [bundle appStoreReceiptURL]; - - // Read in the contents of the transaction file. - receipt = [NSData dataWithContentsOfURL:receiptFileURL]; - sdk_version = 7; + int sdk_version = [[[UIDevice currentDevice] systemVersion] intValue]; - } else { - // Fall back to deprecated transaction receipt, - // which is still available in iOS 7. + NSBundle *bundle = [NSBundle mainBundle]; + // Get the transaction receipt file path location in the app bundle. + NSURL *receiptFileURL = [bundle appStoreReceiptURL]; - // Use SKPaymentTransaction's transactionReceipt. - receipt = transaction.transactionReceipt; - } - - } else { - receipt = transaction.transactionReceipt; - } + // Read in the contents of the transaction file. + receipt = [NSData dataWithContentsOfURL:receiptFileURL]; NSString *receipt_to_send = nil; + if (receipt != nil) { receipt_to_send = [receipt base64EncodedStringWithOptions:0]; } @@ -216,13 +266,13 @@ Error InAppStore::restore_purchases() { InAppStore::get_singleton()->_post_event(ret); - if (auto_finish_transactions) { + if (self.shouldAutoFinishTransactions) { [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } else { - [pending_transactions setObject:transaction forKey:transaction.payment.productIdentifier]; + self.pendingTransactions[transaction.payment.productIdentifier] = transaction; } - }; break; + } break; case SKPaymentTransactionStateFailed: { printf("status transaction failed!\n"); String pid = String::utf8([transaction.payment.productIdentifier UTF8String]); @@ -247,80 +297,119 @@ Error InAppStore::restore_purchases() { } break; default: { printf("status default %i!\n", (int)transaction.transactionState); - }; break; - }; - }; -}; + } break; + } + } +} @end -Error InAppStore::purchase(Variant p_params) { +void InAppStore::_bind_methods() { + ClassDB::bind_method(D_METHOD("request_product_info"), &InAppStore::request_product_info); + ClassDB::bind_method(D_METHOD("restore_purchases"), &InAppStore::restore_purchases); + ClassDB::bind_method(D_METHOD("purchase"), &InAppStore::purchase); + + ClassDB::bind_method(D_METHOD("get_pending_event_count"), &InAppStore::get_pending_event_count); + ClassDB::bind_method(D_METHOD("pop_pending_event"), &InAppStore::pop_pending_event); + ClassDB::bind_method(D_METHOD("finish_transaction"), &InAppStore::finish_transaction); + ClassDB::bind_method(D_METHOD("set_auto_finish_transaction"), &InAppStore::set_auto_finish_transaction); +} + +Error InAppStore::request_product_info(Dictionary p_params) { + ERR_FAIL_COND_V(!p_params.has("product_ids"), ERR_INVALID_PARAMETER); + + PackedStringArray pids = p_params["product_ids"]; + printf("************ request product info! %i\n", pids.size()); + + NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:pids.size()]; + for (int i = 0; i < pids.size(); i++) { + printf("******** adding %s to product list\n", pids[i].utf8().get_data()); + NSString *pid = [[NSString alloc] initWithUTF8String:pids[i].utf8().get_data()]; + [array addObject:pid]; + }; + + NSSet *products = [[NSSet alloc] initWithArray:array]; + + [products_request_delegate performRequestWithProductIDs:products]; + + return OK; +} + +Error InAppStore::restore_purchases() { + printf("restoring purchases!\n"); + [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; + + return OK; +} + +Error InAppStore::purchase(Dictionary p_params) { ERR_FAIL_COND_V(![SKPaymentQueue canMakePayments], ERR_UNAVAILABLE); - if (![SKPaymentQueue canMakePayments]) + if (![SKPaymentQueue canMakePayments]) { return ERR_UNAVAILABLE; + } printf("purchasing!\n"); Dictionary params = p_params; ERR_FAIL_COND_V(!params.has("product_id"), ERR_INVALID_PARAMETER); - NSString *pid = [[[NSString alloc] initWithUTF8String:String(params["product_id"]).utf8().get_data()] autorelease]; - SKPayment *payment = [SKPayment paymentWithProductIdentifier:pid]; - SKPaymentQueue *defq = [SKPaymentQueue defaultQueue]; - [defq addPayment:payment]; - printf("purchase sent!\n"); + NSString *pid = [[NSString alloc] initWithUTF8String:String(params["product_id"]).utf8().get_data()]; - return OK; -}; + return [products_request_delegate purchaseProductWithProductID:pid]; +} int InAppStore::get_pending_event_count() { return pending_events.size(); -}; +} Variant InAppStore::pop_pending_event() { Variant front = pending_events.front()->get(); pending_events.pop_front(); return front; -}; +} void InAppStore::_post_event(Variant p_event) { pending_events.push_back(p_event); -}; +} void InAppStore::_record_purchase(String product_id) { String skey = "purchased/" + product_id; - NSString *key = [[[NSString alloc] initWithUTF8String:skey.utf8().get_data()] autorelease]; + NSString *key = [[NSString alloc] initWithUTF8String:skey.utf8().get_data()]; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:key]; [[NSUserDefaults standardUserDefaults] synchronize]; -}; +} InAppStore *InAppStore::get_singleton() { return instance; -}; +} InAppStore::InAppStore() { ERR_FAIL_COND(instance != NULL); instance = this; - auto_finish_transactions = false; - TransObserver *observer = [[TransObserver alloc] init]; - [[SKPaymentQueue defaultQueue] addTransactionObserver:observer]; - //pending_transactions = [NSMutableDictionary dictionary]; -}; + products_request_delegate = [[GodotProductsDelegate alloc] init]; + transactions_observer = [[GodotTransactionsObserver alloc] init]; + + [[SKPaymentQueue defaultQueue] addTransactionObserver:transactions_observer]; +} void InAppStore::finish_transaction(String product_id) { NSString *prod_id = [NSString stringWithCString:product_id.utf8().get_data() encoding:NSUTF8StringEncoding]; - if ([pending_transactions objectForKey:prod_id]) { - [[SKPaymentQueue defaultQueue] finishTransaction:[pending_transactions objectForKey:prod_id]]; - [pending_transactions removeObjectForKey:prod_id]; - } -}; + [transactions_observer finishTransactionWithProductID:prod_id]; +} void InAppStore::set_auto_finish_transaction(bool b) { - auto_finish_transactions = b; + transactions_observer.shouldAutoFinishTransactions = b; } -InAppStore::~InAppStore() {} +InAppStore::~InAppStore() { + [products_request_delegate reset]; + [transactions_observer reset]; + + products_request_delegate = nil; + [[SKPaymentQueue defaultQueue] removeTransactionObserver:transactions_observer]; + transactions_observer = nil; +} #endif diff --git a/platform/iphone/ios.h b/platform/iphone/ios.h index 2b29e6f268..6a89cd38cb 100644 --- a/platform/iphone/ios.h +++ b/platform/iphone/ios.h @@ -31,7 +31,7 @@ #ifndef IOS_H #define IOS_H -#include "core/object.h" +#include "core/class_db.h" class iOS : public Object { GDCLASS(iOS, Object); diff --git a/platform/iphone/ios.mm b/platform/iphone/ios.mm index 5923f558a5..d4e099063f 100644 --- a/platform/iphone/ios.mm +++ b/platform/iphone/ios.mm @@ -29,17 +29,28 @@ /*************************************************************************/ #include "ios.h" -#include <sys/sysctl.h> - +#import "app_delegate.h" +#import "view_controller.h" #import <UIKit/UIKit.h> +#include <sys/sysctl.h> void iOS::_bind_methods() { ClassDB::bind_method(D_METHOD("get_rate_url", "app_id"), &iOS::get_rate_url); }; void iOS::alert(const char *p_alert, const char *p_title) { - UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:[NSString stringWithUTF8String:p_title] message:[NSString stringWithUTF8String:p_alert] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil] autorelease]; - [alert show]; + NSString *title = [NSString stringWithUTF8String:p_title]; + NSString *message = [NSString stringWithUTF8String:p_alert]; + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *button = [UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleCancel + handler:^(id){ + }]; + + [alert addAction:button]; + + [AppDelegate.viewController presentViewController:alert animated:YES completion:nil]; } String iOS::get_model() const { @@ -58,25 +69,11 @@ String iOS::get_model() const { } String iOS::get_rate_url(int p_app_id) const { - String templ = "itms-apps://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=APP_ID"; - String templ_iOS7 = "itms-apps://itunes.apple.com/app/idAPP_ID"; - String templ_iOS8 = "itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=APP_ID&onlyLatestVersion=true&pageNumber=0&sortOrdering=1&type=Purple+Software"; - - //ios7 before - String ret = templ; - - if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0 && [[[UIDevice currentDevice] systemVersion] floatValue] < 7.1) { - // iOS 7 needs a different templateReviewURL @see https://github.com/arashpayan/appirater/issues/131 - ret = templ_iOS7; - } else if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) { - // iOS 8 needs a different templateReviewURL also @see https://github.com/arashpayan/appirater/issues/182 - ret = templ_iOS8; - } + String app_url_path = "itms-apps://itunes.apple.com/app/idAPP_ID"; - // ios7 for everything? - ret = templ_iOS7.replace("APP_ID", String::num(p_app_id)); + String ret = app_url_path.replace("APP_ID", String::num(p_app_id)); - printf("returning rate url %ls\n", ret.c_str()); + printf("returning rate url %s\n", ret.utf8().get_data()); return ret; }; diff --git a/platform/iphone/joypad_iphone.h b/platform/iphone/joypad_iphone.h new file mode 100644 index 0000000000..85e26e1dc8 --- /dev/null +++ b/platform/iphone/joypad_iphone.h @@ -0,0 +1,50 @@ +/*************************************************************************/ +/* joypad_iphone.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 <GameController/GameController.h> + +@interface JoypadIPhoneObserver : NSObject + +- (void)startObserving; +- (void)startProcessing; +- (void)finishObserving; + +@end + +class JoypadIPhone { +private: + JoypadIPhoneObserver *observer; + +public: + JoypadIPhone(); + ~JoypadIPhone(); + + void start_processing(); +}; diff --git a/platform/iphone/joypad_iphone.mm b/platform/iphone/joypad_iphone.mm new file mode 100644 index 0000000000..b0a8076b56 --- /dev/null +++ b/platform/iphone/joypad_iphone.mm @@ -0,0 +1,345 @@ +/*************************************************************************/ +/* joypad_iphone.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "joypad_iphone.h" +#include "core/project_settings.h" +#include "drivers/coreaudio/audio_driver_coreaudio.h" +#include "main/main.h" + +#import "godot_view.h" + +#include "os_iphone.h" + +JoypadIPhone::JoypadIPhone() { + observer = [[JoypadIPhoneObserver alloc] init]; + [observer startObserving]; +} + +JoypadIPhone::~JoypadIPhone() { + if (observer) { + [observer finishObserving]; + observer = nil; + } +} + +void JoypadIPhone::start_processing() { + if (observer) { + [observer startProcessing]; + } +} + +@interface JoypadIPhoneObserver () + +@property(assign, nonatomic) BOOL isObserving; +@property(assign, nonatomic) BOOL isProcessing; +@property(strong, nonatomic) NSMutableDictionary *connectedJoypads; +@property(strong, nonatomic) NSMutableArray *joypadsQueue; + +@end + +@implementation JoypadIPhoneObserver + +- (instancetype)init { + self = [super init]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.isObserving = NO; + self.isProcessing = NO; +} + +- (void)startProcessing { + self.isProcessing = YES; + + for (GCController *controller in self.joypadsQueue) { + [self addiOSJoypad:controller]; + } + + [self.joypadsQueue removeAllObjects]; +} + +- (void)startObserving { + if (self.isObserving) { + return; + } + + self.isObserving = YES; + + self.connectedJoypads = [NSMutableDictionary dictionary]; + self.joypadsQueue = [NSMutableArray array]; + + // get told when controllers connect, this will be called right away for + // already connected controllers + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(controllerWasConnected:) + name:GCControllerDidConnectNotification + object:nil]; + + // get told when controllers disconnect + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(controllerWasDisconnected:) + name:GCControllerDidDisconnectNotification + object:nil]; +} + +- (void)finishObserving { + if (self.isObserving) { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + } + + self.isObserving = NO; + self.isProcessing = NO; + + self.connectedJoypads = nil; + self.joypadsQueue = nil; +} + +- (void)dealloc { + [self finishObserving]; +} + +- (int)getJoyIdForController:(GCController *)controller { + NSArray *keys = [self.connectedJoypads allKeysForObject:controller]; + + for (NSNumber *key in keys) { + int joy_id = [key intValue]; + return joy_id; + }; + + return -1; +}; + +- (void)addiOSJoypad:(GCController *)controller { + // get a new id for our controller + int joy_id = Input::get_singleton()->get_unused_joy_id(); + + if (joy_id == -1) { + printf("Couldn't retrieve new joy id\n"); + return; + } + + // assign our player index + if (controller.playerIndex == GCControllerPlayerIndexUnset) { + controller.playerIndex = [self getFreePlayerIndex]; + }; + + // tell Godot about our new controller + Input::get_singleton()->joy_connection_changed(joy_id, true, [controller.vendorName UTF8String]); + + // add it to our dictionary, this will retain our controllers + [self.connectedJoypads setObject:controller forKey:[NSNumber numberWithInt:joy_id]]; + + // set our input handler + [self setControllerInputHandler:controller]; +} + +- (void)controllerWasConnected:(NSNotification *)notification { + // get our controller + GCController *controller = (GCController *)notification.object; + + if (!controller) { + printf("Couldn't retrieve new controller\n"); + return; + } + + if ([[self.connectedJoypads allKeysForObject:controller] count] > 0) { + printf("Controller is already registered\n"); + } else if (!self.isProcessing) { + [self.joypadsQueue addObject:controller]; + } else { + [self addiOSJoypad:controller]; + } +} + +- (void)controllerWasDisconnected:(NSNotification *)notification { + // find our joystick, there should be only one in our dictionary + GCController *controller = (GCController *)notification.object; + + if (!controller) { + return; + } + + NSArray *keys = [self.connectedJoypads allKeysForObject:controller]; + for (NSNumber *key in keys) { + // tell Godot this joystick is no longer there + int joy_id = [key intValue]; + Input::get_singleton()->joy_connection_changed(joy_id, false, ""); + + // and remove it from our dictionary + [self.connectedJoypads removeObjectForKey:key]; + }; +}; + +- (GCControllerPlayerIndex)getFreePlayerIndex { + bool have_player_1 = false; + bool have_player_2 = false; + bool have_player_3 = false; + bool have_player_4 = false; + + if (self.connectedJoypads == nil) { + NSArray *keys = [self.connectedJoypads allKeys]; + for (NSNumber *key in keys) { + GCController *controller = [self.connectedJoypads objectForKey:key]; + if (controller.playerIndex == GCControllerPlayerIndex1) { + have_player_1 = true; + } else if (controller.playerIndex == GCControllerPlayerIndex2) { + have_player_2 = true; + } else if (controller.playerIndex == GCControllerPlayerIndex3) { + have_player_3 = true; + } else if (controller.playerIndex == GCControllerPlayerIndex4) { + have_player_4 = true; + }; + }; + }; + + if (!have_player_1) { + return GCControllerPlayerIndex1; + } else if (!have_player_2) { + return GCControllerPlayerIndex2; + } else if (!have_player_3) { + return GCControllerPlayerIndex3; + } else if (!have_player_4) { + return GCControllerPlayerIndex4; + } else { + return GCControllerPlayerIndexUnset; + }; +} + +- (void)setControllerInputHandler:(GCController *)controller { + // Hook in the callback handler for the correct gamepad profile. + // This is a bit of a weird design choice on Apples part. + // You need to select the most capable gamepad profile for the + // gamepad attached. + if (controller.extendedGamepad != nil) { + // The extended gamepad profile has all the input you could possibly find on + // a gamepad but will only be active if your gamepad actually has all of + // these... + _weakify(self); + _weakify(controller); + + controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) { + _strongify(self); + _strongify(controller); + + int joy_id = [self getJoyIdForController:controller]; + + if (element == gamepad.buttonA) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, + gamepad.buttonA.isPressed); + } else if (element == gamepad.buttonB) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_B, + gamepad.buttonB.isPressed); + } else if (element == gamepad.buttonX) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, + gamepad.buttonX.isPressed); + } else if (element == gamepad.buttonY) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_Y, + gamepad.buttonY.isPressed); + } else if (element == gamepad.leftShoulder) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_LEFT_SHOULDER, + gamepad.leftShoulder.isPressed); + } else if (element == gamepad.rightShoulder) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_RIGHT_SHOULDER, + gamepad.rightShoulder.isPressed); + } else if (element == gamepad.dpad) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, + gamepad.dpad.up.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, + gamepad.dpad.down.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, + gamepad.dpad.left.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, + gamepad.dpad.right.isPressed); + }; + + Input::JoyAxis jx; + jx.min = -1; + if (element == gamepad.leftThumbstick) { + jx.value = gamepad.leftThumbstick.xAxis.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_LEFT_X, jx); + jx.value = -gamepad.leftThumbstick.yAxis.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_LEFT_Y, jx); + } else if (element == gamepad.rightThumbstick) { + jx.value = gamepad.rightThumbstick.xAxis.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_RIGHT_X, jx); + jx.value = -gamepad.rightThumbstick.yAxis.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_RIGHT_Y, jx); + } else if (element == gamepad.leftTrigger) { + jx.value = gamepad.leftTrigger.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_LEFT, jx); + } else if (element == gamepad.rightTrigger) { + jx.value = gamepad.rightTrigger.value; + Input::get_singleton()->joy_axis(joy_id, JOY_AXIS_TRIGGER_RIGHT, jx); + }; + }; + } else if (controller.microGamepad != nil) { + // micro gamepads were added in OS 9 and feature just 2 buttons and a d-pad + _weakify(self); + _weakify(controller); + + controller.microGamepad.valueChangedHandler = ^(GCMicroGamepad *gamepad, GCControllerElement *element) { + _strongify(self); + _strongify(controller); + + int joy_id = [self getJoyIdForController:controller]; + + if (element == gamepad.buttonA) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_A, + gamepad.buttonA.isPressed); + } else if (element == gamepad.buttonX) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_X, + gamepad.buttonX.isPressed); + } else if (element == gamepad.dpad) { + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_UP, + gamepad.dpad.up.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_DOWN, + gamepad.dpad.down.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_LEFT, gamepad.dpad.left.isPressed); + Input::get_singleton()->joy_button(joy_id, JOY_BUTTON_DPAD_RIGHT, gamepad.dpad.right.isPressed); + }; + }; + } + + ///@TODO need to add support for controller.motion which gives us access to + /// the orientation of the device (if supported) + + ///@TODO need to add support for controllerPausedHandler which should be a + /// toggle +}; + +@end diff --git a/platform/iphone/logo.png b/platform/iphone/logo.png Binary files differindex 405b6f93ca..966d8aa70a 100644 --- a/platform/iphone/logo.png +++ b/platform/iphone/logo.png diff --git a/platform/iphone/main.m b/platform/iphone/main.m index 164db2a74b..c292f02822 100644 --- a/platform/iphone/main.m +++ b/platform/iphone/main.m @@ -32,20 +32,25 @@ #import <UIKit/UIKit.h> #include <stdio.h> +#include <vulkan/vulkan.h> int gargc; char **gargv; int main(int argc, char *argv[]) { +#if defined(VULKAN_ENABLED) + //MoltenVK - enable full component swizzling support + setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); +#endif + printf("*********** main.m\n"); gargc = argc; gargv = argv; - NSAutoreleasePool *pool = [NSAutoreleasePool new]; - AppDelegate *app = [AppDelegate alloc]; printf("running app main\n"); - UIApplicationMain(argc, argv, nil, @"AppDelegate"); - printf("main done, pool release\n"); - [pool release]; + @autoreleasepool { + UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } + printf("main done\n"); return 0; } diff --git a/platform/iphone/native_video_view.h b/platform/iphone/native_video_view.h new file mode 100644 index 0000000000..d8687b3538 --- /dev/null +++ b/platform/iphone/native_video_view.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* native_video_view.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 <UIKit/UIKit.h> + +@interface GodotNativeVideoView : UIView + +- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack; +- (BOOL)isVideoPlaying; +- (void)pauseVideo; +- (void)unpauseVideo; +- (void)stopVideo; + +@end diff --git a/platform/iphone/native_video_view.m b/platform/iphone/native_video_view.m new file mode 100644 index 0000000000..a4e9f209f0 --- /dev/null +++ b/platform/iphone/native_video_view.m @@ -0,0 +1,260 @@ +/*************************************************************************/ +/* native_video_view.m */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 "native_video_view.h" +#import <AVFoundation/AVFoundation.h> + +@interface GodotNativeVideoView () + +@property(strong, nonatomic) AVAsset *avAsset; +@property(strong, nonatomic) AVPlayerItem *avPlayerItem; +@property(strong, nonatomic) AVPlayer *avPlayer; +@property(strong, nonatomic) AVPlayerLayer *avPlayerLayer; +@property(assign, nonatomic) CMTime videoCurrentTime; +@property(assign, nonatomic) BOOL isVideoCurrentlyPlaying; + +@end + +@implementation GodotNativeVideoView + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + self.isVideoCurrentlyPlaying = NO; + self.videoCurrentTime = kCMTimeZero; + + [self observeVideoAudio]; +} + +- (void)observeVideoAudio { + printf("******** adding observer for sound routing changes\n"); + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(audioRouteChangeListenerCallback:) + name:AVAudioSessionRouteChangeNotification + object:nil]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (object == self.avPlayerItem && [keyPath isEqualToString:@"status"]) { + [self handleVideoOrPlayerStatus]; + } + + if (object == self.avPlayer && [keyPath isEqualToString:@"rate"]) { + [self handleVideoPlayRate]; + } +} + +// MARK: Video Audio + +- (void)audioRouteChangeListenerCallback:(NSNotification *)notification { + printf("*********** route changed!\n"); + NSDictionary *interuptionDict = notification.userInfo; + + NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue]; + + switch (routeChangeReason) { + case AVAudioSessionRouteChangeReasonNewDeviceAvailable: { + NSLog(@"AVAudioSessionRouteChangeReasonNewDeviceAvailable"); + NSLog(@"Headphone/Line plugged in"); + } break; + case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: { + NSLog(@"AVAudioSessionRouteChangeReasonOldDeviceUnavailable"); + NSLog(@"Headphone/Line was pulled. Resuming video play...."); + if ([self isVideoPlaying]) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self.avPlayer play]; // NOTE: change this line according your current player implementation + NSLog(@"resumed play"); + }); + } + } break; + case AVAudioSessionRouteChangeReasonCategoryChange: { + // called at start - also when other audio wants to play + NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange"); + } break; + } +} + +// MARK: Native Video Player + +- (void)handleVideoOrPlayerStatus { + if (self.avPlayerItem.status == AVPlayerItemStatusFailed || self.avPlayer.status == AVPlayerStatusFailed) { + [self stopVideo]; + } + + if (self.avPlayer.status == AVPlayerStatusReadyToPlay && self.avPlayerItem.status == AVPlayerItemStatusReadyToPlay && CMTimeCompare(self.videoCurrentTime, kCMTimeZero) == 0) { + // NSLog(@"time: %@", self.video_current_time); + [self.avPlayer seekToTime:self.videoCurrentTime]; + self.videoCurrentTime = kCMTimeZero; + } +} + +- (void)handleVideoPlayRate { + NSLog(@"Player playback rate changed: %.5f", self.avPlayer.rate); + if ([self isVideoPlaying] && self.avPlayer.rate == 0.0 && !self.avPlayer.error) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self.avPlayer play]; // NOTE: change this line according your current player implementation + NSLog(@"resumed play"); + }); + + NSLog(@" . . . PAUSED (or just started)"); + } +} + +- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack { + self.avAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:filePath]]; + + self.avPlayerItem = [AVPlayerItem playerItemWithAsset:self.avAsset]; + [self.avPlayerItem addObserver:self forKeyPath:@"status" options:0 context:nil]; + + self.avPlayer = [AVPlayer playerWithPlayerItem:self.avPlayerItem]; + self.avPlayerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer]; + + [self.avPlayer addObserver:self forKeyPath:@"status" options:0 context:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(playerItemDidReachEnd:) + name:AVPlayerItemDidPlayToEndTimeNotification + object:[self.avPlayer currentItem]]; + + [self.avPlayer addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:0]; + + [self.avPlayerLayer setFrame:self.bounds]; + [self.layer addSublayer:self.avPlayerLayer]; + [self.avPlayer play]; + + AVMediaSelectionGroup *audioGroup = [self.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible]; + + NSMutableArray *allAudioParams = [NSMutableArray array]; + for (id track in audioGroup.options) { + NSString *language = [[track locale] localeIdentifier]; + NSLog(@"subtitle lang: %@", language); + + if ([language isEqualToString:audioTrack]) { + AVMutableAudioMixInputParameters *audioInputParams = [AVMutableAudioMixInputParameters audioMixInputParameters]; + [audioInputParams setVolume:videoVolume atTime:kCMTimeZero]; + [audioInputParams setTrackID:[track trackID]]; + [allAudioParams addObject:audioInputParams]; + + AVMutableAudioMix *audioMix = [AVMutableAudioMix audioMix]; + [audioMix setInputParameters:allAudioParams]; + + [self.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:audioGroup]; + [self.avPlayer.currentItem setAudioMix:audioMix]; + + break; + } + } + + AVMediaSelectionGroup *subtitlesGroup = [self.avAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; + NSArray *useableTracks = [AVMediaSelectionGroup mediaSelectionOptionsFromArray:subtitlesGroup.options withoutMediaCharacteristics:[NSArray arrayWithObject:AVMediaCharacteristicContainsOnlyForcedSubtitles]]; + + for (id track in useableTracks) { + NSString *language = [[track locale] localeIdentifier]; + NSLog(@"subtitle lang: %@", language); + + if ([language isEqualToString:subtitleTrack]) { + [self.avPlayer.currentItem selectMediaOption:track inMediaSelectionGroup:subtitlesGroup]; + break; + } + } + + self.isVideoCurrentlyPlaying = YES; + + return true; +} + +- (BOOL)isVideoPlaying { + if (self.avPlayer.error) { + printf("Error during playback\n"); + } + return (self.avPlayer.rate > 0 && !self.avPlayer.error); +} + +- (void)pauseVideo { + self.videoCurrentTime = self.avPlayer.currentTime; + [self.avPlayer pause]; + self.isVideoCurrentlyPlaying = NO; +} + +- (void)unpauseVideo { + [self.avPlayer play]; + self.isVideoCurrentlyPlaying = YES; +} + +- (void)playerItemDidReachEnd:(NSNotification *)notification { + [self stopVideo]; +} + +- (void)finishPlayingVideo { + [self.avPlayer pause]; + [self.avPlayerLayer removeFromSuperlayer]; + self.avPlayerLayer = nil; + + if (self.avPlayerItem) { + [self.avPlayerItem removeObserver:self forKeyPath:@"status"]; + self.avPlayerItem = nil; + } + + if (self.avPlayer) { + [self.avPlayer removeObserver:self forKeyPath:@"status"]; + self.avPlayer = nil; + } + + self.avAsset = nil; + + self.isVideoCurrentlyPlaying = NO; +} + +- (void)stopVideo { + [self finishPlayingVideo]; + + [self removeFromSuperview]; +} + +@end diff --git a/platform/iphone/os_iphone.cpp b/platform/iphone/os_iphone.cpp deleted file mode 100644 index 41dd623e69..0000000000 --- a/platform/iphone/os_iphone.cpp +++ /dev/null @@ -1,632 +0,0 @@ -/*************************************************************************/ -/* os_iphone.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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. */ -/*************************************************************************/ - -#ifdef IPHONE_ENABLED - -#include "os_iphone.h" - -#if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" -#endif - -#if defined(VULKAN_ENABLED) -#include "servers/rendering/rasterizer_rd/rasterizer_rd.h" -// #import <QuartzCore/CAMetalLayer.h> -#include <vulkan/vulkan_metal.h> -#endif - -#include "servers/rendering/rendering_server_raster.h" -#include "servers/rendering/rendering_server_wrap_mt.h" - -#include "main/main.h" - -#include "core/io/file_access_pack.h" -#include "core/os/dir_access.h" -#include "core/os/file_access.h" -#include "core/project_settings.h" -#include "drivers/unix/syslog_logger.h" - -#include "semaphore_iphone.h" - -#include <dlfcn.h> - -int OSIPhone::get_video_driver_count() const { - return 2; -}; - -const char *OSIPhone::get_video_driver_name(int p_driver) const { - switch (p_driver) { - case VIDEO_DRIVER_GLES2: - return "GLES2"; - } - ERR_FAIL_V_MSG(nullptr, "Invalid video driver index: " + itos(p_driver) + "."); -}; - -OSIPhone *OSIPhone::get_singleton() { - return (OSIPhone *)OS::get_singleton(); -}; - -extern int gl_view_base_fb; // from gl_view.mm - -void OSIPhone::set_data_dir(String p_dir) { - DirAccess *da = DirAccess::open(p_dir); - - data_dir = da->get_current_dir(); - printf("setting data dir to %ls from %ls\n", data_dir.c_str(), p_dir.c_str()); - memdelete(da); -}; - -void OSIPhone::set_unique_id(String p_id) { - unique_id = p_id; -}; - -String OSIPhone::get_unique_id() const { - return unique_id; -}; - -void OSIPhone::initialize_core() { - OS_Unix::initialize_core(); - - set_data_dir(data_dir); -}; - -int OSIPhone::get_current_video_driver() const { - return video_driver_index; -} - -Error OSIPhone::initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver) { - video_driver_index = p_video_driver; - -#if defined(OPENGL_ENABLED) - bool gl_initialization_error = false; - - // FIXME: Add Vulkan support via MoltenVK. Add fallback code back? - - if (RasterizerGLES2::is_viable() == OK) { - RasterizerGLES2::register_config(); - RasterizerGLES2::make_current(); - } else { - gl_initialization_error = true; - } - - if (gl_initialization_error) { - OS::get_singleton()->alert("Your device does not support any of the supported OpenGL versions.", - "Unable to initialize video driver"); - return ERR_UNAVAILABLE; - } -#endif - -#if defined(VULKAN_ENABLED) - RasterizerRD::make_current(); -#endif - - rendering_server = memnew(RenderingServerRaster); - // FIXME: Reimplement threaded rendering - if (get_render_thread_mode() != RENDER_THREAD_UNSAFE) { - rendering_server = memnew(RenderingServerWrapMT(rendering_server, false)); - } - rendering_server->init(); - //rendering_server->cursor_set_visible(false, 0); - -#if defined(OPENGL_ENABLED) - // reset this to what it should be, it will have been set to 0 after rendering_server->init() is called - RasterizerStorageGLES2::system_fbo = gl_view_base_fb; -#endif - - AudioDriverManager::initialize(p_audio_driver); - - input = memnew(InputDefault); - -#ifdef GAME_CENTER_ENABLED - game_center = memnew(GameCenter); - Engine::get_singleton()->add_singleton(Engine::Singleton("GameCenter", game_center)); - game_center->connect(); -#endif - -#ifdef STOREKIT_ENABLED - store_kit = memnew(InAppStore); - Engine::get_singleton()->add_singleton(Engine::Singleton("InAppStore", store_kit)); -#endif - -#ifdef ICLOUD_ENABLED - icloud = memnew(ICloud); - Engine::get_singleton()->add_singleton(Engine::Singleton("ICloud", icloud)); - //icloud->connect(); -#endif - ios = memnew(iOS); - Engine::get_singleton()->add_singleton(Engine::Singleton("iOS", ios)); - - return OK; -}; - -MainLoop *OSIPhone::get_main_loop() const { - return main_loop; -}; - -void OSIPhone::set_main_loop(MainLoop *p_main_loop) { - main_loop = p_main_loop; - - if (main_loop) { - input->set_main_loop(p_main_loop); - main_loop->init(); - } -}; - -bool OSIPhone::iterate() { - if (!main_loop) - return true; - - if (main_loop) { - for (int i = 0; i < event_count; i++) { - input->parse_input_event(event_queue[i]); - }; - }; - event_count = 0; - - return Main::iteration(); -}; - -void OSIPhone::key(uint32_t p_key, bool p_pressed) { - Ref<InputEventKey> ev; - ev.instance(); - ev->set_echo(false); - ev->set_pressed(p_pressed); - ev->set_keycode(p_key); - ev->set_physical_keycode(p_key); - ev->set_unicode(p_key); - queue_event(ev); -}; - -void OSIPhone::touch_press(int p_idx, int p_x, int p_y, bool p_pressed, bool p_doubleclick) { - if (!GLOBAL_DEF("debug/disable_touch", false)) { - Ref<InputEventScreenTouch> ev; - ev.instance(); - - ev->set_index(p_idx); - ev->set_pressed(p_pressed); - ev->set_position(Vector2(p_x, p_y)); - queue_event(ev); - }; - - touch_list.pressed[p_idx] = p_pressed; -}; - -void OSIPhone::touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y) { - if (!GLOBAL_DEF("debug/disable_touch", false)) { - Ref<InputEventScreenDrag> ev; - ev.instance(); - ev->set_index(p_idx); - ev->set_position(Vector2(p_x, p_y)); - ev->set_relative(Vector2(p_x - p_prev_x, p_y - p_prev_y)); - queue_event(ev); - }; -}; - -void OSIPhone::queue_event(const Ref<InputEvent> &p_event) { - ERR_FAIL_INDEX(event_count, MAX_EVENTS); - - event_queue[event_count++] = p_event; -}; - -void OSIPhone::touches_cancelled() { - for (int i = 0; i < MAX_MOUSE_COUNT; i++) { - if (touch_list.pressed[i]) { - // send a mouse_up outside the screen - touch_press(i, -1, -1, false, false); - }; - }; -}; - -static const float ACCEL_RANGE = 1; - -void OSIPhone::update_gravity(float p_x, float p_y, float p_z) { - input->set_gravity(Vector3(p_x, p_y, p_z)); -}; - -void OSIPhone::update_accelerometer(float p_x, float p_y, float p_z) { - // Found out the Z should not be negated! Pass as is! - input->set_accelerometer(Vector3(p_x / (float)ACCEL_RANGE, p_y / (float)ACCEL_RANGE, p_z / (float)ACCEL_RANGE)); - - /* - if (p_x != last_accel.x) { - //printf("updating accel x %f\n", p_x); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_0; - ev.joy_motion.axis_value = (p_x / (float)ACCEL_RANGE); - last_accel.x = p_x; - queue_event(ev); - }; - if (p_y != last_accel.y) { - //printf("updating accel y %f\n", p_y); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_1; - ev.joy_motion.axis_value = (p_y / (float)ACCEL_RANGE); - last_accel.y = p_y; - queue_event(ev); - }; - if (p_z != last_accel.z) { - //printf("updating accel z %f\n", p_z); - InputEvent ev; - ev.type = InputEvent::JOYPAD_MOTION; - ev.device = 0; - ev.joy_motion.axis = JOY_ANALOG_2; - ev.joy_motion.axis_value = ( (1.0 - p_z) / (float)ACCEL_RANGE); - last_accel.z = p_z; - queue_event(ev); - }; - */ -}; - -void OSIPhone::update_magnetometer(float p_x, float p_y, float p_z) { - input->set_magnetometer(Vector3(p_x, p_y, p_z)); -}; - -void OSIPhone::update_gyroscope(float p_x, float p_y, float p_z) { - input->set_gyroscope(Vector3(p_x, p_y, p_z)); -}; - -int OSIPhone::get_unused_joy_id() { - return input->get_unused_joy_id(); -}; - -void OSIPhone::joy_connection_changed(int p_idx, bool p_connected, String p_name) { - input->joy_connection_changed(p_idx, p_connected, p_name); -}; - -void OSIPhone::joy_button(int p_device, int p_button, bool p_pressed) { - input->joy_button(p_device, p_button, p_pressed); -}; - -void OSIPhone::joy_axis(int p_device, int p_axis, const InputDefault::JoyAxis &p_value) { - input->joy_axis(p_device, p_axis, p_value); -}; - -void OSIPhone::delete_main_loop() { - if (main_loop) { - main_loop->finish(); - memdelete(main_loop); - }; - - main_loop = nullptr; -}; - -void OSIPhone::finalize() { - delete_main_loop(); - - memdelete(input); - memdelete(ios); - -#ifdef GAME_CENTER_ENABLED - memdelete(game_center); -#endif - -#ifdef STOREKIT_ENABLED - memdelete(store_kit); -#endif - -#ifdef ICLOUD_ENABLED - memdelete(icloud); -#endif - - rendering_server->finish(); - memdelete(rendering_server); - // memdelete(rasterizer); - - // Free unhandled events before close - for (int i = 0; i < MAX_EVENTS; i++) { - event_queue[i].unref(); - }; - event_count = 0; -}; - -void OSIPhone::set_mouse_show(bool p_show) {} -void OSIPhone::set_mouse_grab(bool p_grab) {} - -bool OSIPhone::is_mouse_grab_enabled() const { - return true; -}; - -Point2 OSIPhone::get_mouse_position() const { - return Point2(); -}; - -int OSIPhone::get_mouse_button_state() const { - return 0; -}; - -void OSIPhone::set_window_title(const String &p_title) {} - -void OSIPhone::alert(const String &p_alert, const String &p_title) { - const CharString utf8_alert = p_alert.utf8(); - const CharString utf8_title = p_title.utf8(); - iOS::alert(utf8_alert.get_data(), utf8_title.get_data()); -} - -Error OSIPhone::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { - if (p_path.length() == 0) { - p_library_handle = RTLD_SELF; - return OK; - } - return OS_Unix::open_dynamic_library(p_path, p_library_handle, p_also_set_library_path); -} - -Error OSIPhone::close_dynamic_library(void *p_library_handle) { - if (p_library_handle == RTLD_SELF) { - return OK; - } - return OS_Unix::close_dynamic_library(p_library_handle); -} - -HashMap<String, void *> OSIPhone::dynamic_symbol_lookup_table; -void register_dynamic_symbol(char *name, void *address) { - OSIPhone::dynamic_symbol_lookup_table[String(name)] = address; -} - -Error OSIPhone::get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional) { - if (p_library_handle == RTLD_SELF) { - void **ptr = OSIPhone::dynamic_symbol_lookup_table.getptr(p_name); - if (ptr) { - p_symbol_handle = *ptr; - return OK; - } - } - return OS_Unix::get_dynamic_library_symbol_handle(p_library_handle, p_name, p_symbol_handle, p_optional); -} - -void OSIPhone::set_video_mode(const VideoMode &p_video_mode, int p_screen) { - video_mode = p_video_mode; -}; - -OS::VideoMode OSIPhone::get_video_mode(int p_screen) const { - return video_mode; -}; - -void OSIPhone::get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen) const { - p_list->push_back(video_mode); -}; - -bool OSIPhone::can_draw() const { - if (native_video_is_playing()) - return false; - return true; -}; - -int OSIPhone::set_base_framebuffer(int p_fb) { -#if defined(OPENGL_ENABLED) - // gl_view_base_fb has not been updated yet - RasterizerStorageGLES2::system_fbo = p_fb; -#endif - - return 0; -}; - -bool OSIPhone::has_virtual_keyboard() const { - return true; -}; - -extern void _show_keyboard(String p_existing); -extern void _hide_keyboard(); -extern Error _shell_open(String p_uri); -extern void _set_keep_screen_on(bool p_enabled); -extern void _vibrate(); - -void OSIPhone::show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect, int p_max_input_length, int p_cursor_start, int p_cursor_end) { - _show_keyboard(p_existing_text); -}; - -void OSIPhone::hide_virtual_keyboard() { - _hide_keyboard(); -}; - -void OSIPhone::set_virtual_keyboard_height(int p_height) { - virtual_keyboard_height = p_height; -} - -int OSIPhone::get_virtual_keyboard_height() const { - return virtual_keyboard_height; -} - -Error OSIPhone::shell_open(String p_uri) { - return _shell_open(p_uri); -}; - -void OSIPhone::set_keep_screen_on(bool p_enabled) { - OS::set_keep_screen_on(p_enabled); - _set_keep_screen_on(p_enabled); -}; - -String OSIPhone::get_user_data_dir() const { - return data_dir; -}; - -String OSIPhone::get_name() const { - return "iOS"; -}; - -String OSIPhone::get_model_name() const { - String model = ios->get_model(); - if (model != "") - return model; - - return OS_Unix::get_model_name(); -} - -Size2 OSIPhone::get_window_size() const { - return Vector2(video_mode.width, video_mode.height); -} - -extern Rect2 _get_ios_window_safe_area(float p_window_width, float p_window_height); - -Rect2 OSIPhone::get_window_safe_area() const { - return _get_ios_window_safe_area(video_mode.width, video_mode.height); -} - -bool OSIPhone::has_touchscreen_ui_hint() const { - return true; -} - -void OSIPhone::set_locale(String p_locale) { - locale_code = p_locale; -} - -String OSIPhone::get_locale() const { - return locale_code; -} - -extern bool _play_video(String p_path, float p_volume, String p_audio_track, String p_subtitle_track); -extern bool _is_video_playing(); -extern void _pause_video(); -extern void _unpause_video(); -extern void _stop_video(); -extern void _focus_out_video(); - -Error OSIPhone::native_video_play(String p_path, float p_volume, String p_audio_track, String p_subtitle_track) { - FileAccess *f = FileAccess::open(p_path, FileAccess::READ); - bool exists = f && f->is_open(); - - String tempFile = get_user_data_dir(); - if (!exists) - return FAILED; - - if (p_path.begins_with("res://")) { - if (PackedData::get_singleton()->has_path(p_path)) { - print("Unable to play %S using the native player as it resides in a .pck file\n", p_path.c_str()); - return ERR_INVALID_PARAMETER; - } else { - p_path = p_path.replace("res:/", ProjectSettings::get_singleton()->get_resource_path()); - } - } else if (p_path.begins_with("user://")) - p_path = p_path.replace("user:/", get_user_data_dir()); - - memdelete(f); - - print("Playing video: %S\n", p_path.c_str()); - if (_play_video(p_path, p_volume, p_audio_track, p_subtitle_track)) - return OK; - return FAILED; -} - -bool OSIPhone::native_video_is_playing() const { - return _is_video_playing(); -} - -void OSIPhone::native_video_pause() { - if (native_video_is_playing()) - _pause_video(); -} - -void OSIPhone::native_video_unpause() { - _unpause_video(); -}; - -void OSIPhone::native_video_focus_out() { - _focus_out_video(); -}; - -void OSIPhone::native_video_stop() { - if (native_video_is_playing()) - _stop_video(); -} - -void OSIPhone::vibrate_handheld(int p_duration_ms) { - // iOS does not support duration for vibration - _vibrate(); -} - -bool OSIPhone::_check_internal_feature_support(const String &p_feature) { - return p_feature == "mobile"; -} - -// Initialization order between compilation units is not guaranteed, -// so we use this as a hack to ensure certain code is called before -// everything else, but after all units are initialized. -typedef void (*init_callback)(); -static init_callback *ios_init_callbacks = nullptr; -static int ios_init_callbacks_count = 0; -static int ios_init_callbacks_capacity = 0; - -void add_ios_init_callback(init_callback cb) { - if (ios_init_callbacks_count == ios_init_callbacks_capacity) { - void *new_ptr = realloc(ios_init_callbacks, sizeof(cb) * 32); - if (new_ptr) { - ios_init_callbacks = (init_callback *)(new_ptr); - ios_init_callbacks_capacity += 32; - } - } - if (ios_init_callbacks_capacity > ios_init_callbacks_count) { - ios_init_callbacks[ios_init_callbacks_count] = cb; - ++ios_init_callbacks_count; - } -} - -OSIPhone::OSIPhone(int width, int height, String p_data_dir) { - for (int i = 0; i < ios_init_callbacks_count; ++i) { - ios_init_callbacks[i](); - } - free(ios_init_callbacks); - ios_init_callbacks = nullptr; - ios_init_callbacks_count = 0; - ios_init_callbacks_capacity = 0; - - main_loop = nullptr; - rendering_server = nullptr; - - VideoMode vm; - vm.fullscreen = true; - vm.width = width; - vm.height = height; - vm.resizable = false; - set_video_mode(vm); - event_count = 0; - virtual_keyboard_height = 0; - - // can't call set_data_dir from here, since it requires DirAccess - // which is initialized in initialize_core - data_dir = p_data_dir; - - Vector<Logger *> loggers; - loggers.push_back(memnew(SyslogLogger)); -#ifdef DEBUG_ENABLED - // it seems iOS app's stdout/stderr is only obtainable if you launch it from Xcode - loggers.push_back(memnew(StdLogger)); -#endif - _set_logger(memnew(CompositeLogger(loggers))); - - AudioDriverManager::add_driver(&audio_driver); -}; - -OSIPhone::~OSIPhone() { -} - -#endif diff --git a/platform/iphone/os_iphone.h b/platform/iphone/os_iphone.h index 955eb15d57..c6f95869ee 100644 --- a/platform/iphone/os_iphone.h +++ b/platform/iphone/os_iphone.h @@ -33,16 +33,15 @@ #ifndef OS_IPHONE_H #define OS_IPHONE_H -#include "core/input/input.h" #include "drivers/coreaudio/audio_driver_coreaudio.h" #include "drivers/unix/os_unix.h" #include "game_center.h" #include "icloud.h" #include "in_app_store.h" #include "ios.h" +#include "joypad_iphone.h" #include "servers/audio_server.h" #include "servers/rendering/rasterizer.h" -#include "servers/rendering_server.h" #if defined(VULKAN_ENABLED) #include "drivers/vulkan/rendering_device_vulkan.h" @@ -51,16 +50,9 @@ class OSIPhone : public OS_Unix { private: - enum { - MAX_MOUSE_COUNT = 8, - MAX_EVENTS = 64, - }; - static HashMap<String, void *> dynamic_symbol_lookup_table; friend void register_dynamic_symbol(char *name, void *address); - RenderingServer *rendering_server; - AudioDriverCoreAudio audio_driver; #ifdef GAME_CENTER_ENABLED @@ -74,139 +66,68 @@ private: #endif iOS *ios; - MainLoop *main_loop; - -#if defined(VULKAN_ENABLED) - VulkanContextIPhone *context_vulkan; - RenderingDeviceVulkan *rendering_device_vulkan; -#endif - VideoMode video_mode; - - virtual int get_video_driver_count() const; - virtual const char *get_video_driver_name(int p_driver) const; + JoypadIPhone *joypad_iphone; - virtual int get_current_video_driver() const; - - virtual void initialize_core(); - virtual Error initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver); - - virtual void set_main_loop(MainLoop *p_main_loop); - virtual MainLoop *get_main_loop() const; - - virtual void delete_main_loop(); - - virtual void finalize(); + MainLoop *main_loop; - struct MouseList { - bool pressed[MAX_MOUSE_COUNT]; - MouseList() { - for (int i = 0; i < MAX_MOUSE_COUNT; i++) - pressed[i] = false; - }; - }; + virtual void initialize_core() override; + virtual void initialize() override; - MouseList touch_list; + virtual void initialize_joypads() override { + } - Vector3 last_accel; + virtual void set_main_loop(MainLoop *p_main_loop) override; + virtual MainLoop *get_main_loop() const override; - Ref<InputEvent> event_queue[MAX_EVENTS]; - int event_count; - void queue_event(const Ref<InputEvent> &p_event); + virtual void delete_main_loop() override; - String data_dir; - String unique_id; - String locale_code; + virtual void finalize() override; - InputDefault *input; + String user_data_dir; - int virtual_keyboard_height; + bool is_focused = false; - int video_driver_index; + void deinitialize_modules(); public: - bool iterate(); - - uint8_t get_orientations() const; - - void touch_press(int p_idx, int p_x, int p_y, bool p_pressed, bool p_doubleclick); - void touch_drag(int p_idx, int p_prev_x, int p_prev_y, int p_x, int p_y); - void touches_cancelled(); - void key(uint32_t p_key, bool p_pressed); - void set_virtual_keyboard_height(int p_height); - - int set_base_framebuffer(int p_fb); - - void update_gravity(float p_x, float p_y, float p_z); - void update_accelerometer(float p_x, float p_y, float p_z); - void update_magnetometer(float p_x, float p_y, float p_z); - void update_gyroscope(float p_x, float p_y, float p_z); - - int get_unused_joy_id(); - void joy_connection_changed(int p_idx, bool p_connected, String p_name); - void joy_button(int p_device, int p_button, bool p_pressed); - void joy_axis(int p_device, int p_axis, const InputDefault::JoyAxis &p_value); - static OSIPhone *get_singleton(); - virtual void set_mouse_show(bool p_show); - virtual void set_mouse_grab(bool p_grab); - virtual bool is_mouse_grab_enabled() const; - virtual Point2 get_mouse_position() const; - virtual int get_mouse_button_state() const; - virtual void set_window_title(const String &p_title); - - virtual void alert(const String &p_alert, const String &p_title = "ALERT!"); - - virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false); - virtual Error close_dynamic_library(void *p_library_handle); - virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false); - - virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0); - virtual VideoMode get_video_mode(int p_screen = 0) const; - virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const; + OSIPhone(String p_data_dir); + ~OSIPhone(); - virtual void set_keep_screen_on(bool p_enabled); + void initialize_modules(); - virtual bool can_draw() const; + bool iterate(); - virtual bool has_virtual_keyboard() const; - virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); - virtual void hide_virtual_keyboard(); - virtual int get_virtual_keyboard_height() const; + void start(); - virtual Size2 get_window_size() const; - virtual Rect2 get_window_safe_area() const; + virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false) override; + virtual Error close_dynamic_library(void *p_library_handle) override; + virtual Error get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional = false) override; - virtual bool has_touchscreen_ui_hint() const; + virtual void alert(const String &p_alert, + const String &p_title = "ALERT!") override; - void set_data_dir(String p_dir); + virtual String get_name() const override; + virtual String get_model_name() const override; - virtual String get_name() const; - virtual String get_model_name() const; + virtual Error shell_open(String p_uri) override; - Error shell_open(String p_uri); + void set_user_data_dir(String p_dir); + virtual String get_user_data_dir() const override; - String get_user_data_dir() const; + virtual String get_locale() const override; - void set_locale(String p_locale); - String get_locale() const; + virtual String get_unique_id() const override; - void set_unique_id(String p_id); - String get_unique_id() const; + virtual void vibrate_handheld(int p_duration_ms = 500) override; - virtual Error native_video_play(String p_path, float p_volume, String p_audio_track, String p_subtitle_track); - virtual bool native_video_is_playing() const; - virtual void native_video_pause(); - virtual void native_video_unpause(); - virtual void native_video_focus_out(); - virtual void native_video_stop(); - virtual void vibrate_handheld(int p_duration_ms = 500); + virtual bool _check_internal_feature_support(const String &p_feature) override; - virtual bool _check_internal_feature_support(const String &p_feature); - OSIPhone(int width, int height, String p_data_dir); - ~OSIPhone(); + void on_focus_out(); + void on_focus_in(); }; #endif // OS_IPHONE_H -#endif +#endif // IPHONE_ENABLED diff --git a/platform/iphone/os_iphone.mm b/platform/iphone/os_iphone.mm new file mode 100644 index 0000000000..e007276b4b --- /dev/null +++ b/platform/iphone/os_iphone.mm @@ -0,0 +1,359 @@ +/*************************************************************************/ +/* os_iphone.mm */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#ifdef IPHONE_ENABLED + +#include "os_iphone.h" +#import "app_delegate.h" +#include "core/io/file_access_pack.h" +#include "core/os/dir_access.h" +#include "core/os/file_access.h" +#include "core/project_settings.h" +#include "display_server_iphone.h" +#include "drivers/unix/syslog_logger.h" +#import "godot_view.h" +#include "main/main.h" +#import "view_controller.h" + +#import <AudioToolbox/AudioServices.h> +#import <UIKit/UIKit.h> +#import <dlfcn.h> + +#if defined(VULKAN_ENABLED) +#include "servers/rendering/rasterizer_rd/rasterizer_rd.h" +#import <QuartzCore/CAMetalLayer.h> +#include <vulkan/vulkan_metal.h> +#endif + +// Initialization order between compilation units is not guaranteed, +// so we use this as a hack to ensure certain code is called before +// everything else, but after all units are initialized. +typedef void (*init_callback)(); +static init_callback *ios_init_callbacks = nullptr; +static int ios_init_callbacks_count = 0; +static int ios_init_callbacks_capacity = 0; +HashMap<String, void *> OSIPhone::dynamic_symbol_lookup_table; + +void add_ios_init_callback(init_callback cb) { + if (ios_init_callbacks_count == ios_init_callbacks_capacity) { + void *new_ptr = realloc(ios_init_callbacks, sizeof(cb) * 32); + if (new_ptr) { + ios_init_callbacks = (init_callback *)(new_ptr); + ios_init_callbacks_capacity += 32; + } + } + if (ios_init_callbacks_capacity > ios_init_callbacks_count) { + ios_init_callbacks[ios_init_callbacks_count] = cb; + ++ios_init_callbacks_count; + } +} + +void register_dynamic_symbol(char *name, void *address) { + OSIPhone::dynamic_symbol_lookup_table[String(name)] = address; +} + +OSIPhone *OSIPhone::get_singleton() { + return (OSIPhone *)OS::get_singleton(); +} + +OSIPhone::OSIPhone(String p_data_dir) { + for (int i = 0; i < ios_init_callbacks_count; ++i) { + ios_init_callbacks[i](); + } + free(ios_init_callbacks); + ios_init_callbacks = nullptr; + ios_init_callbacks_count = 0; + ios_init_callbacks_capacity = 0; + + main_loop = nullptr; + + // can't call set_data_dir from here, since it requires DirAccess + // which is initialized in initialize_core + user_data_dir = p_data_dir; + + Vector<Logger *> loggers; + loggers.push_back(memnew(SyslogLogger)); +#ifdef DEBUG_ENABLED + // it seems iOS app's stdout/stderr is only obtainable if you launch it from + // Xcode + loggers.push_back(memnew(StdLogger)); +#endif + _set_logger(memnew(CompositeLogger(loggers))); + + AudioDriverManager::add_driver(&audio_driver); + + DisplayServerIPhone::register_iphone_driver(); +} + +OSIPhone::~OSIPhone() {} + +void OSIPhone::initialize_core() { + OS_Unix::initialize_core(); + + set_user_data_dir(user_data_dir); +} + +void OSIPhone::initialize() { + initialize_core(); +} + +void OSIPhone::initialize_modules() { +#ifdef GAME_CENTER_ENABLED + game_center = memnew(GameCenter); + Engine::get_singleton()->add_singleton(Engine::Singleton("GameCenter", game_center)); +#endif + +#ifdef STOREKIT_ENABLED + store_kit = memnew(InAppStore); + Engine::get_singleton()->add_singleton(Engine::Singleton("InAppStore", store_kit)); +#endif + +#ifdef ICLOUD_ENABLED + icloud = memnew(ICloud); + Engine::get_singleton()->add_singleton(Engine::Singleton("ICloud", icloud)); +#endif + + ios = memnew(iOS); + Engine::get_singleton()->add_singleton(Engine::Singleton("iOS", ios)); + + joypad_iphone = memnew(JoypadIPhone); +} + +void OSIPhone::deinitialize_modules() { + if (joypad_iphone) { + memdelete(joypad_iphone); + } + + if (ios) { + memdelete(ios); + } + +#ifdef GAME_CENTER_ENABLED + if (game_center) { + memdelete(game_center); + } +#endif + +#ifdef STOREKIT_ENABLED + if (store_kit) { + memdelete(store_kit); + } +#endif + +#ifdef ICLOUD_ENABLED + if (icloud) { + memdelete(icloud); + } +#endif +} + +void OSIPhone::set_main_loop(MainLoop *p_main_loop) { + main_loop = p_main_loop; + + if (main_loop) { + main_loop->init(); + } +} + +MainLoop *OSIPhone::get_main_loop() const { + return main_loop; +} + +void OSIPhone::delete_main_loop() { + if (main_loop) { + main_loop->finish(); + memdelete(main_loop); + }; + + main_loop = nullptr; +} + +bool OSIPhone::iterate() { + if (!main_loop) { + return true; + } + + if (DisplayServer::get_singleton()) { + DisplayServer::get_singleton()->process_events(); + } + + return Main::iteration(); +} + +void OSIPhone::start() { + Main::start(); + + if (joypad_iphone) { + joypad_iphone->start_processing(); + } +} + +void OSIPhone::finalize() { + deinitialize_modules(); + + // Already gets called + // delete_main_loop(); +} + +// MARK: Dynamic Libraries + +Error OSIPhone::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { + if (p_path.length() == 0) { + p_library_handle = RTLD_SELF; + return OK; + } + return OS_Unix::open_dynamic_library(p_path, p_library_handle, p_also_set_library_path); +} + +Error OSIPhone::close_dynamic_library(void *p_library_handle) { + if (p_library_handle == RTLD_SELF) { + return OK; + } + return OS_Unix::close_dynamic_library(p_library_handle); +} + +Error OSIPhone::get_dynamic_library_symbol_handle(void *p_library_handle, const String p_name, void *&p_symbol_handle, bool p_optional) { + if (p_library_handle == RTLD_SELF) { + void **ptr = OSIPhone::dynamic_symbol_lookup_table.getptr(p_name); + if (ptr) { + p_symbol_handle = *ptr; + return OK; + } + } + return OS_Unix::get_dynamic_library_symbol_handle(p_library_handle, p_name, p_symbol_handle, p_optional); +} + +void OSIPhone::alert(const String &p_alert, const String &p_title) { + const CharString utf8_alert = p_alert.utf8(); + const CharString utf8_title = p_title.utf8(); + iOS::alert(utf8_alert.get_data(), utf8_title.get_data()); +} + +String OSIPhone::get_name() const { + return "iOS"; +}; + +String OSIPhone::get_model_name() const { + String model = ios->get_model(); + if (model != "") + return model; + + return OS_Unix::get_model_name(); +} + +Error OSIPhone::shell_open(String p_uri) { + NSString *urlPath = [[NSString alloc] initWithUTF8String:p_uri.utf8().get_data()]; + NSURL *url = [NSURL URLWithString:urlPath]; + + if (![[UIApplication sharedApplication] canOpenURL:url]) { + return ERR_CANT_OPEN; + } + + printf("opening url %s\n", p_uri.utf8().get_data()); + + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; + + return OK; +}; + +void OSIPhone::set_user_data_dir(String p_dir) { + DirAccess *da = DirAccess::open(p_dir); + + user_data_dir = da->get_current_dir(); + printf("setting data dir to %s from %s\n", user_data_dir.utf8().get_data(), p_dir.utf8().get_data()); + memdelete(da); +} + +String OSIPhone::get_user_data_dir() const { + return user_data_dir; +} + +String OSIPhone::get_locale() const { + NSString *preferedLanguage = [NSLocale preferredLanguages].firstObject; + + if (preferedLanguage) { + return String::utf8([preferedLanguage UTF8String]).replace("-", "_"); + } + + NSString *localeIdentifier = [[NSLocale currentLocale] localeIdentifier]; + return String::utf8([localeIdentifier UTF8String]).replace("-", "_"); +} + +String OSIPhone::get_unique_id() const { + NSString *uuid = [UIDevice currentDevice].identifierForVendor.UUIDString; + return String::utf8([uuid UTF8String]); +} + +void OSIPhone::vibrate_handheld(int p_duration_ms) { + // iOS does not support duration for vibration + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); +} + +bool OSIPhone::_check_internal_feature_support(const String &p_feature) { + return p_feature == "mobile"; +} + +void OSIPhone::on_focus_out() { + if (is_focused) { + is_focused = false; + + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT); + } + + [AppDelegate.viewController.godotView stopRendering]; + + if (DisplayServerIPhone::get_singleton() && DisplayServerIPhone::get_singleton()->native_video_is_playing()) { + DisplayServerIPhone::get_singleton()->native_video_pause(); + } + + audio_driver.stop(); + } +} + +void OSIPhone::on_focus_in() { + if (!is_focused) { + is_focused = true; + + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_IN); + } + + [AppDelegate.viewController.godotView startRendering]; + + if (DisplayServerIPhone::get_singleton() && DisplayServerIPhone::get_singleton()->native_video_is_playing()) { + DisplayServerIPhone::get_singleton()->native_video_unpause(); + } + + audio_driver.start(); + } +} + +#endif // IPHONE_ENABLED diff --git a/platform/iphone/platform_config.h b/platform/iphone/platform_config.h index bc190ba956..ec39ad0ba4 100644 --- a/platform/iphone/platform_config.h +++ b/platform/iphone/platform_config.h @@ -30,8 +30,13 @@ #include <alloca.h> -#define GLES2_INCLUDE_H <ES2/gl.h> - #define PLATFORM_REFCOUNT #define PTHREAD_RENAME_SELF + +#define _weakify(var) __weak typeof(var) GDWeak_##var = var; +#define _strongify(var) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wshadow\"") \ + __strong typeof(var) var = GDWeak_##var; \ + _Pragma("clang diagnostic pop") diff --git a/platform/iphone/view_controller.h b/platform/iphone/view_controller.h index f6bbe11d97..b0b31ae377 100644 --- a/platform/iphone/view_controller.h +++ b/platform/iphone/view_controller.h @@ -31,20 +31,16 @@ #import <GameKit/GameKit.h> #import <UIKit/UIKit.h> -@interface ViewController : UIViewController <GKGameCenterControllerDelegate> { -}; +@class GodotView; +@class GodotNativeVideoView; -- (BOOL)shouldAutorotateToInterfaceOrientation: - (UIInterfaceOrientation)p_orientation; +@interface ViewController : UIViewController <GKGameCenterControllerDelegate> -- (void)didReceiveMemoryWarning; +@property(nonatomic, readonly, strong) GodotView *godotView; +@property(nonatomic, readonly, strong) GodotNativeVideoView *videoView; -- (void)viewDidLoad; +// MARK: Native Video Player -- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures; - -- (BOOL)prefersStatusBarHidden; - -- (BOOL)prefersHomeIndicatorAutoHidden; +- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack; @end diff --git a/platform/iphone/view_controller.mm b/platform/iphone/view_controller.mm index 279bcc1226..fb25041779 100644 --- a/platform/iphone/view_controller.mm +++ b/platform/iphone/view_controller.mm @@ -29,96 +29,140 @@ /*************************************************************************/ #import "view_controller.h" - +#include "core/project_settings.h" +#include "display_server_iphone.h" +#import "godot_view.h" +#import "godot_view_renderer.h" +#import "native_video_view.h" #include "os_iphone.h" -#include "core/project_settings.h" +#import <GameController/GameController.h> -extern "C" { +@interface ViewController () -int add_path(int, char **); -int add_cmdline(int, char **); +@property(strong, nonatomic) GodotViewRenderer *renderer; +@property(strong, nonatomic) GodotNativeVideoView *videoView; -int add_path(int p_argc, char **p_args) { - NSString *str = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_path"]; - if (!str) - return p_argc; +@end - p_args[p_argc++] = "--path"; - [str retain]; // memory leak lol (maybe make it static here and delete it in ViewController destructor? @todo - p_args[p_argc++] = (char *)[str cString]; - p_args[p_argc] = NULL; +@implementation ViewController - return p_argc; -}; +- (GodotView *)godotView { + return (GodotView *)self.view; +} -int add_cmdline(int p_argc, char **p_args) { - NSArray *arr = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"godot_cmdline"]; - if (!arr) - return p_argc; +- (void)loadView { + GodotView *view = [[GodotView alloc] init]; + GodotViewRenderer *renderer = [[GodotViewRenderer alloc] init]; - for (int i = 0; i < [arr count]; i++) { - NSString *str = [arr objectAtIndex:i]; - if (!str) - continue; - [str retain]; // @todo delete these at some point - p_args[p_argc++] = (char *)[str cString]; - }; + self.renderer = renderer; + self.view = view; - p_args[p_argc] = NULL; + view.renderer = self.renderer; +} - return p_argc; -}; -}; // extern "C" +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; -@interface ViewController () + if (self) { + [self godot_commonInit]; + } -@end + return self; +} -@implementation ViewController +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + + if (self) { + [self godot_commonInit]; + } + + return self; +} + +- (void)godot_commonInit { + // Initialize view controller values. +} - (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; printf("*********** did receive memory warning!\n"); -}; +} - (void)viewDidLoad { [super viewDidLoad]; + [self observeKeyboard]; + if (@available(iOS 11.0, *)) { [self setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; } } +- (void)observeKeyboard { + printf("******** adding observer for keyboard show/hide\n"); + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(keyboardOnScreen:) + name:UIKeyboardDidShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(keyboardHidden:) + name:UIKeyboardDidHideNotification + object:nil]; +} + +- (void)dealloc { + [self.videoView stopVideo]; + + self.videoView = nil; + self.renderer = nil; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +// MARK: Orientation + - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures { return UIRectEdgeAll; } - (BOOL)shouldAutorotate { - switch (OS::get_singleton()->get_screen_orientation()) { - case OS::SCREEN_SENSOR: - case OS::SCREEN_SENSOR_LANDSCAPE: - case OS::SCREEN_SENSOR_PORTRAIT: + if (!DisplayServerIPhone::get_singleton()) { + return NO; + } + + switch (DisplayServerIPhone::get_singleton()->screen_get_orientation(DisplayServer::SCREEN_OF_MAIN_WINDOW)) { + case DisplayServer::SCREEN_SENSOR: + case DisplayServer::SCREEN_SENSOR_LANDSCAPE: + case DisplayServer::SCREEN_SENSOR_PORTRAIT: return YES; default: return NO; } -}; +} - (UIInterfaceOrientationMask)supportedInterfaceOrientations { - switch (OS::get_singleton()->get_screen_orientation()) { - case OS::SCREEN_PORTRAIT: + if (!DisplayServerIPhone::get_singleton()) { + return UIInterfaceOrientationMaskAll; + } + + switch (DisplayServerIPhone::get_singleton()->screen_get_orientation(DisplayServer::SCREEN_OF_MAIN_WINDOW)) { + case DisplayServer::SCREEN_PORTRAIT: return UIInterfaceOrientationMaskPortrait; - case OS::SCREEN_REVERSE_LANDSCAPE: + case DisplayServer::SCREEN_REVERSE_LANDSCAPE: return UIInterfaceOrientationMaskLandscapeRight; - case OS::SCREEN_REVERSE_PORTRAIT: + case DisplayServer::SCREEN_REVERSE_PORTRAIT: return UIInterfaceOrientationMaskPortraitUpsideDown; - case OS::SCREEN_SENSOR_LANDSCAPE: + case DisplayServer::SCREEN_SENSOR_LANDSCAPE: return UIInterfaceOrientationMaskLandscape; - case OS::SCREEN_SENSOR_PORTRAIT: + case DisplayServer::SCREEN_SENSOR_PORTRAIT: return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown; - case OS::SCREEN_SENSOR: + case DisplayServer::SCREEN_SENSOR: return UIInterfaceOrientationMaskAll; - case OS::SCREEN_LANDSCAPE: + case DisplayServer::SCREEN_LANDSCAPE: return UIInterfaceOrientationMaskLandscapeLeft; } }; @@ -135,6 +179,43 @@ int add_cmdline(int p_argc, char **p_args) { } } +// MARK: Keyboard + +- (void)keyboardOnScreen:(NSNotification *)notification { + NSDictionary *info = notification.userInfo; + NSValue *value = info[UIKeyboardFrameEndUserInfoKey]; + + CGRect rawFrame = [value CGRectValue]; + CGRect keyboardFrame = [self.view convertRect:rawFrame fromView:nil]; + + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->virtual_keyboard_set_height(keyboardFrame.size.height); + } +} + +- (void)keyboardHidden:(NSNotification *)notification { + if (DisplayServerIPhone::get_singleton()) { + DisplayServerIPhone::get_singleton()->virtual_keyboard_set_height(0); + } +} + +// MARK: Native Video Player + +- (BOOL)playVideoAtPath:(NSString *)filePath volume:(float)videoVolume audio:(NSString *)audioTrack subtitle:(NSString *)subtitleTrack { + // If we are showing some video already, reuse existing view for new video. + if (self.videoView) { + return [self.videoView playVideoAtPath:filePath volume:videoVolume audio:audioTrack subtitle:subtitleTrack]; + } else { + // Create autoresizing view for video playback. + GodotNativeVideoView *videoView = [[GodotNativeVideoView alloc] initWithFrame:self.view.bounds]; + videoView.autoresizingMask = UIViewAutoresizingFlexibleWidth & UIViewAutoresizingFlexibleHeight; + [self.view addSubview:videoView]; + return [self.videoView playVideoAtPath:filePath volume:videoVolume audio:audioTrack subtitle:subtitleTrack]; + } +} + +// MARK: Delegates + #ifdef GAME_CENTER_ENABLED - (void)gameCenterViewControllerDidFinish:(GKGameCenterViewController *)gameCenterViewController { //[gameCenterViewController dismissViewControllerAnimated:YES completion:^{GameCenter::get_singleton()->game_center_closed();}];//version for signaling when overlay is completely gone diff --git a/platform/iphone/vulkan_context_iphone.h b/platform/iphone/vulkan_context_iphone.h index cadd701636..5c3d5fe33e 100644 --- a/platform/iphone/vulkan_context_iphone.h +++ b/platform/iphone/vulkan_context_iphone.h @@ -32,13 +32,14 @@ #define VULKAN_CONTEXT_IPHONE_H #include "drivers/vulkan/vulkan_context.h" -// #import <UIKit/UIKit.h> + +#import <UIKit/UIKit.h> class VulkanContextIPhone : public VulkanContext { virtual const char *_get_platform_surface_extension() const; public: - int window_create(void *p_window, int p_width, int p_height); + Error window_create(DisplayServer::WindowID p_window_id, CALayer *p_metal_layer, int p_width, int p_height); VulkanContextIPhone(); ~VulkanContextIPhone(); diff --git a/platform/iphone/vulkan_context_iphone.mm b/platform/iphone/vulkan_context_iphone.mm index 44c940dc3a..d62e826957 100644 --- a/platform/iphone/vulkan_context_iphone.mm +++ b/platform/iphone/vulkan_context_iphone.mm @@ -35,21 +35,21 @@ const char *VulkanContextIPhone::_get_platform_surface_extension() const { return VK_MVK_IOS_SURFACE_EXTENSION_NAME; } -int VulkanContextIPhone::window_create(void *p_window, int p_width, int p_height) { +Error VulkanContextIPhone::window_create(DisplayServer::WindowID p_window_id, CALayer *p_metal_layer, int p_width, int p_height) { VkIOSSurfaceCreateInfoMVK createInfo; - createInfo.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK; + createInfo.sType = VK_STRUCTURE_TYPE_IOS_SURFACE_CREATE_INFO_MVK; createInfo.pNext = NULL; createInfo.flags = 0; - createInfo.pView = p_window; + createInfo.pView = (__bridge const void *)p_metal_layer; VkSurfaceKHR surface; - VkResult err = vkCreateIOSSurfaceMVK(_get_instance(), &createInfo, NULL, &surface); - ERR_FAIL_COND_V(err, -1); - return _window_create(surface, p_width, p_height); -} + VkResult err = + vkCreateIOSSurfaceMVK(_get_instance(), &createInfo, NULL, &surface); + ERR_FAIL_COND_V(err, ERR_CANT_CREATE); -VulkanContextIPhone::VulkanContextIPhone() { + return _window_create(p_window_id, surface, p_width, p_height); } -VulkanContextIPhone::~VulkanContextIPhone() { -} +VulkanContextIPhone::VulkanContextIPhone() {} + +VulkanContextIPhone::~VulkanContextIPhone() {} diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index dcf9a46bf9..7381ea13b7 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -9,6 +9,7 @@ javascript_files = [ "javascript_eval.cpp", "javascript_main.cpp", "os_javascript.cpp", + "api/javascript_tools_editor_plugin.cpp", ] build_targets = ["#bin/godot${PROGSUFFIX}.js", "#bin/godot${PROGSUFFIX}.wasm"] @@ -19,6 +20,7 @@ build = env.add_program(build_targets, javascript_files) js_libraries = [ "native/http_request.js", + "native/library_godot_audio.js", ] for lib in js_libraries: env.Append(LINKFLAGS=["--js-library", env.File(lib).path]) @@ -54,7 +56,7 @@ out_files = [ zip_dir.File(binary_name + ".wasm"), zip_dir.File(binary_name + ".html"), ] -html_file = "#misc/dist/html/full-size.html" +html_file = "#misc/dist/html/editor.html" if env["tools"] else "#misc/dist/html/full-size.html" in_files = [js_wrapped, build[1], html_file] if env["threads_enabled"]: in_files.append(build[2]) @@ -66,5 +68,5 @@ env.Zip( zip_files, ZIPROOT=zip_dir, ZIPSUFFIX="${PROGSUFFIX}${ZIPSUFFIX}", - ZIPCOMSTR="Archving $SOURCES as $TARGET", + ZIPCOMSTR="Archiving $SOURCES as $TARGET", ) diff --git a/platform/javascript/api/api.cpp b/platform/javascript/api/api.cpp index 9c73e5c4c4..aa0206d144 100644 --- a/platform/javascript/api/api.cpp +++ b/platform/javascript/api/api.cpp @@ -31,10 +31,12 @@ #include "api.h" #include "core/engine.h" #include "javascript_eval.h" +#include "javascript_tools_editor_plugin.h" static JavaScript *javascript_eval; void register_javascript_api() { + JavaScriptToolsEditorPlugin::initialize(); ClassDB::register_virtual_class<JavaScript>(); javascript_eval = memnew(JavaScript); Engine::get_singleton()->add_singleton(Engine::Singleton("JavaScript", javascript_eval)); diff --git a/platform/javascript/api/javascript_eval.h b/platform/javascript/api/javascript_eval.h index 29229de8e3..26b5b9e484 100644 --- a/platform/javascript/api/javascript_eval.h +++ b/platform/javascript/api/javascript_eval.h @@ -31,7 +31,7 @@ #ifndef JAVASCRIPT_EVAL_H #define JAVASCRIPT_EVAL_H -#include "core/object.h" +#include "core/class_db.h" class JavaScript : public Object { private: diff --git a/platform/javascript/api/javascript_tools_editor_plugin.cpp b/platform/javascript/api/javascript_tools_editor_plugin.cpp new file mode 100644 index 0000000000..e487bf23b7 --- /dev/null +++ b/platform/javascript/api/javascript_tools_editor_plugin.cpp @@ -0,0 +1,153 @@ +/*************************************************************************/ +/* javascript_tools_editor_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#if defined(TOOLS_ENABLED) && defined(JAVASCRIPT_ENABLED) +#include "javascript_tools_editor_plugin.h" + +#include "core/engine.h" +#include "core/os/dir_access.h" +#include "core/os/file_access.h" +#include "core/project_settings.h" +#include "editor/editor_node.h" + +#include <emscripten/emscripten.h> + +static void _javascript_editor_init_callback() { + EditorNode::get_singleton()->add_editor_plugin(memnew(JavaScriptToolsEditorPlugin(EditorNode::get_singleton()))); +} + +void JavaScriptToolsEditorPlugin::initialize() { + EditorNode::add_init_callback(_javascript_editor_init_callback); +} + +JavaScriptToolsEditorPlugin::JavaScriptToolsEditorPlugin(EditorNode *p_editor) { + Variant v; + add_tool_menu_item("Download Project Source", this, "_download_zip", v); +} + +void JavaScriptToolsEditorPlugin::_download_zip(Variant p_v) { + if (!Engine::get_singleton() || !Engine::get_singleton()->is_editor_hint()) { + WARN_PRINT("Project download is only available in Editor mode"); + return; + } + String resource_path = ProjectSettings::get_singleton()->get_resource_path(); + + FileAccess *src_f; + zlib_filefunc_def io = zipio_create_io_from_file(&src_f); + zipFile zip = zipOpen2("/tmp/project.zip", APPEND_STATUS_CREATE, NULL, &io); + String base_path = resource_path.substr(0, resource_path.rfind("/")) + "/"; + _zip_recursive(resource_path, base_path, zip); + zipClose(zip, NULL); + EM_ASM({ + const path = "/tmp/project.zip"; + const size = FS.stat(path)["size"]; + const buf = new Uint8Array(size); + const fd = FS.open(path, "r"); + FS.read(fd, buf, 0, size); + FS.close(fd); + FS.unlink(path); + const blob = new Blob([buf], { type: "application/zip" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "project.zip"; + a.style.display = "none"; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + }); +} + +void JavaScriptToolsEditorPlugin::_bind_methods() { + ClassDB::bind_method("_download_zip", &JavaScriptToolsEditorPlugin::_download_zip); +} + +void JavaScriptToolsEditorPlugin::_zip_file(String p_path, String p_base_path, zipFile p_zip) { + FileAccess *f = FileAccess::open(p_path, FileAccess::READ); + if (!f) { + WARN_PRINT("Unable to open file for zipping: " + p_path); + return; + } + Vector<uint8_t> data; + int len = f->get_len(); + data.resize(len); + f->get_buffer(data.ptrw(), len); + f->close(); + memdelete(f); + + String path = p_path.replace_first(p_base_path, ""); + zipOpenNewFileInZip(p_zip, + path.utf8().get_data(), + NULL, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + Z_DEFAULT_COMPRESSION); + zipWriteInFileInZip(p_zip, data.ptr(), data.size()); + zipCloseFileInZip(p_zip); +} + +void JavaScriptToolsEditorPlugin::_zip_recursive(String p_path, String p_base_path, zipFile p_zip) { + DirAccess *dir = DirAccess::open(p_path); + if (!dir) { + WARN_PRINT("Unable to open dir for zipping: " + p_path); + return; + } + dir->list_dir_begin(); + String cur = dir->get_next(); + while (!cur.empty()) { + String cs = p_path.plus_file(cur); + if (cur == "." || cur == ".." || cur == ".import") { + // Skip + } else if (dir->current_is_dir()) { + String path = cs.replace_first(p_base_path, "") + "/"; + zipOpenNewFileInZip(p_zip, + path.utf8().get_data(), + NULL, + NULL, + 0, + NULL, + 0, + NULL, + Z_DEFLATED, + Z_DEFAULT_COMPRESSION); + zipCloseFileInZip(p_zip); + _zip_recursive(cs, p_base_path, p_zip); + } else { + _zip_file(cs, p_base_path, p_zip); + } + cur = dir->get_next(); + } +} +#endif diff --git a/platform/javascript/api/javascript_tools_editor_plugin.h b/platform/javascript/api/javascript_tools_editor_plugin.h new file mode 100644 index 0000000000..cc09fa4cd3 --- /dev/null +++ b/platform/javascript/api/javascript_tools_editor_plugin.h @@ -0,0 +1,62 @@ +/*************************************************************************/ +/* javascript_tools_editor_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#ifndef JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H +#define JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H + +#if defined(TOOLS_ENABLED) && defined(JAVASCRIPT_ENABLED) +#include "core/io/zip_io.h" +#include "editor/editor_plugin.h" + +class JavaScriptToolsEditorPlugin : public EditorPlugin { + GDCLASS(JavaScriptToolsEditorPlugin, EditorPlugin); + +private: + void _zip_file(String p_path, String p_base_path, zipFile p_zip); + void _zip_recursive(String p_path, String p_base_path, zipFile p_zip); + +protected: + static void _bind_methods(); + + void _download_zip(Variant p_v); + +public: + static void initialize(); + + JavaScriptToolsEditorPlugin(EditorNode *p_editor); +}; +#else +class JavaScriptToolsEditorPlugin { +public: + static void initialize() {} +}; +#endif + +#endif // JAVASCRIPT_TOOLS_EDITOR_PLUGIN_H diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 9604914b2c..6ea948004e 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -31,34 +31,57 @@ #include "audio_driver_javascript.h" #include "core/project_settings.h" +#include "godot_audio.h" #include <emscripten.h> AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr; bool AudioDriverJavaScript::is_available() { - return EM_ASM_INT({ - if (!(window.AudioContext || window.webkitAudioContext)) { - return 0; - } - return 1; - }) != 0; + return godot_audio_is_available() != 0; } const char *AudioDriverJavaScript::get_name() const { return "JavaScript"; } -extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_js_mix() { - AudioDriverJavaScript::singleton->mix_to_js(); +#ifndef NO_THREADS +void AudioDriverJavaScript::_audio_thread_func(void *p_data) { + AudioDriverJavaScript *obj = static_cast<AudioDriverJavaScript *>(p_data); + while (!obj->quit) { + obj->lock(); + if (!obj->needs_process) { + obj->unlock(); + OS::get_singleton()->delay_usec(1000); // Give the browser some slack. + continue; + } + obj->_js_driver_process(); + obj->needs_process = false; + obj->unlock(); + } +} +#endif + +extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_start() { +#ifndef NO_THREADS + AudioDriverJavaScript::singleton->lock(); +#else + AudioDriverJavaScript::singleton->_js_driver_process(); +#endif +} + +extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_end() { +#ifndef NO_THREADS + AudioDriverJavaScript::singleton->needs_process = true; + AudioDriverJavaScript::singleton->unlock(); +#endif } extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) { AudioDriverJavaScript::singleton->process_capture(sample); } -void AudioDriverJavaScript::mix_to_js() { - int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); +void AudioDriverJavaScript::_js_driver_process() { int sample_count = memarr_len(internal_buffer) / channel_count; int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer); audio_server_process(sample_count, stream_buffer); @@ -73,37 +96,12 @@ void AudioDriverJavaScript::process_capture(float sample) { } Error AudioDriverJavaScript::init() { - int mix_rate = GLOBAL_GET("audio/mix_rate"); + mix_rate = GLOBAL_GET("audio/mix_rate"); int latency = GLOBAL_GET("audio/output_latency"); - /* clang-format off */ - _driver_id = EM_ASM_INT({ - const MIX_RATE = $0; - const LATENCY = $1 / 1000; - return Module.IDHandler.add({ - 'context': new (window.AudioContext || window.webkitAudioContext)({ sampleRate: MIX_RATE, latencyHint: LATENCY}), - 'input': null, - 'stream': null, - 'script': null - }); - }, mix_rate, latency); - /* clang-format on */ - - int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode()); + channel_count = godot_audio_init(mix_rate, latency); buffer_length = closest_power_of_2((latency * mix_rate / 1000) * channel_count); - /* clang-format off */ - buffer_length = EM_ASM_INT({ - var ref = Module.IDHandler.get($0); - const ctx = ref['context']; - const BUFFER_LENGTH = $1; - const CHANNEL_COUNT = $2; - - var script = ctx.createScriptProcessor(BUFFER_LENGTH, 2, CHANNEL_COUNT); - script.connect(ctx.destination); - ref['script'] = script; - return script.bufferSize; - }, _driver_id, buffer_length, channel_count); - /* clang-format on */ + buffer_length = godot_audio_create_processor(buffer_length, channel_count); if (!buffer_length) { return FAILED; } @@ -114,134 +112,60 @@ Error AudioDriverJavaScript::init() { internal_buffer = memnew_arr(float, buffer_length *channel_count); } - return internal_buffer ? OK : ERR_OUT_OF_MEMORY; + if (!internal_buffer) { + return ERR_OUT_OF_MEMORY; + } + return OK; } void AudioDriverJavaScript::start() { - /* clang-format off */ - EM_ASM({ - const ref = Module.IDHandler.get($0); - var INTERNAL_BUFFER_PTR = $1; - - var audioDriverMixFunction = cwrap('audio_driver_js_mix'); - var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); - ref['script'].onaudioprocess = function(audioProcessingEvent) { - audioDriverMixFunction(); - - var input = audioProcessingEvent.inputBuffer; - var output = audioProcessingEvent.outputBuffer; - var internalBuffer = HEAPF32.subarray( - INTERNAL_BUFFER_PTR / HEAPF32.BYTES_PER_ELEMENT, - INTERNAL_BUFFER_PTR / HEAPF32.BYTES_PER_ELEMENT + output.length * output.numberOfChannels); - - for (var channel = 0; channel < output.numberOfChannels; channel++) { - var outputData = output.getChannelData(channel); - // Loop through samples. - for (var sample = 0; sample < outputData.length; sample++) { - outputData[sample] = internalBuffer[sample * output.numberOfChannels + channel]; - } - } - - if (ref['input']) { - var inputDataL = input.getChannelData(0); - var inputDataR = input.getChannelData(1); - for (var i = 0; i < inputDataL.length; i++) { - audioDriverProcessCapture(inputDataL[i]); - audioDriverProcessCapture(inputDataR[i]); - } - } - }; - }, _driver_id, internal_buffer); - /* clang-format on */ +#ifndef NO_THREADS + thread = Thread::create(_audio_thread_func, this); +#endif + godot_audio_start(internal_buffer); } void AudioDriverJavaScript::resume() { - /* clang-format off */ - EM_ASM({ - const ref = Module.IDHandler.get($0); - if (ref && ref['context'] && ref['context'].resume) - ref['context'].resume(); - }, _driver_id); - /* clang-format on */ + godot_audio_resume(); } float AudioDriverJavaScript::get_latency() { - /* clang-format off */ - return EM_ASM_DOUBLE({ - const ref = Module.IDHandler.get($0); - var latency = 0; - if (ref && ref['context']) { - const ctx = ref['context']; - if (ctx.baseLatency) { - latency += ctx.baseLatency; - } - if (ctx.outputLatency) { - latency += ctx.outputLatency; - } - } - return latency; - }, _driver_id); - /* clang-format on */ + return godot_audio_get_latency(); } int AudioDriverJavaScript::get_mix_rate() const { - /* clang-format off */ - return EM_ASM_INT({ - const ref = Module.IDHandler.get($0); - return ref && ref['context'] ? ref['context'].sampleRate : 0; - }, _driver_id); - /* clang-format on */ + return mix_rate; } AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { - /* clang-format off */ - return get_speaker_mode_by_total_channels(EM_ASM_INT({ - const ref = Module.IDHandler.get($0); - return ref && ref['context'] ? ref['context'].destination.channelCount : 0; - }, _driver_id)); - /* clang-format on */ + return get_speaker_mode_by_total_channels(channel_count); } -// No locking, as threads are not supported. void AudioDriverJavaScript::lock() { +#ifndef NO_THREADS + mutex.lock(); +#endif } void AudioDriverJavaScript::unlock() { +#ifndef NO_THREADS + mutex.unlock(); +#endif } void AudioDriverJavaScript::finish_async() { - // Close the context, add the operation to the async_finish list in module. - int id = _driver_id; - _driver_id = 0; - - /* clang-format off */ - EM_ASM({ - const id = $0; - var ref = Module.IDHandler.get(id); - Module.async_finish.push(new Promise(function(accept, reject) { - if (!ref) { - console.log("Ref not found!", id, Module.IDHandler); - setTimeout(accept, 0); - } else { - Module.IDHandler.remove(id); - const context = ref['context']; - // Disconnect script and input. - ref['script'].disconnect(); - if (ref['input']) - ref['input'].disconnect(); - ref = null; - context.close().then(function() { - accept(); - }).catch(function(e) { - accept(); - }); - } - })); - }, id); - /* clang-format on */ +#ifndef NO_THREADS + quit = true; // Ask thread to quit. +#endif + godot_audio_finish_async(); } void AudioDriverJavaScript::finish() { +#ifndef NO_THREADS + Thread::wait_to_finish(thread); + memdelete(thread); + thread = NULL; +#endif if (internal_buffer) { memdelete_arr(internal_buffer); internal_buffer = nullptr; @@ -249,56 +173,14 @@ void AudioDriverJavaScript::finish() { } Error AudioDriverJavaScript::capture_start() { + godot_audio_capture_stop(); input_buffer_init(buffer_length); - - /* clang-format off */ - EM_ASM({ - function gotMediaInput(stream) { - var ref = Module.IDHandler.get($0); - ref['stream'] = stream; - ref['input'] = ref['context'].createMediaStreamSource(stream); - ref['input'].connect(ref['script']); - } - - function gotMediaInputError(e) { - out(e); - } - - if (navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({"audio": true}).then(gotMediaInput, gotMediaInputError); - } else { - if (!navigator.getUserMedia) - navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); - } - }, _driver_id); - /* clang-format on */ - + godot_audio_capture_start(); return OK; } Error AudioDriverJavaScript::capture_stop() { - /* clang-format off */ - EM_ASM({ - var ref = Module.IDHandler.get($0); - if (ref['stream']) { - const tracks = ref['stream'].getTracks(); - for (var i = 0; i < tracks.length; i++) { - tracks[i].stop(); - } - ref['stream'] = null; - } - - if (ref['input']) { - ref['input'].disconnect(); - ref['input'] = null; - } - - }, _driver_id); - /* clang-format on */ - input_buffer.clear(); - return OK; } diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index f029a91db0..56a7da0307 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -33,34 +33,49 @@ #include "servers/audio_server.h" +#include "core/os/mutex.h" +#include "core/os/thread.h" + class AudioDriverJavaScript : public AudioDriver { +private: float *internal_buffer = nullptr; - int _driver_id = 0; int buffer_length = 0; + int mix_rate = 0; + int channel_count = 0; public: +#ifndef NO_THREADS + Mutex mutex; + Thread *thread = nullptr; + bool quit = false; + bool needs_process = true; + + static void _audio_thread_func(void *p_data); +#endif + + void _js_driver_process(); + static bool is_available(); - void mix_to_js(); void process_capture(float sample); static AudioDriverJavaScript *singleton; - virtual const char *get_name() const; + const char *get_name() const override; - virtual Error init(); - virtual void start(); + Error init() override; + void start() override; void resume(); - virtual float get_latency(); - virtual int get_mix_rate() const; - virtual SpeakerMode get_speaker_mode() const; - virtual void lock(); - virtual void unlock(); - virtual void finish(); + float get_latency() override; + int get_mix_rate() const override; + SpeakerMode get_speaker_mode() const override; + void lock() override; + void unlock() override; + void finish() override; void finish_async(); - virtual Error capture_start(); - virtual Error capture_stop(); + Error capture_start() override; + Error capture_stop() override; AudioDriverJavaScript(); }; diff --git a/platform/javascript/detect.py b/platform/javascript/detect.py index 81287cead8..8f2961b33d 100644 --- a/platform/javascript/detect.py +++ b/platform/javascript/detect.py @@ -1,6 +1,7 @@ import os -from emscripten_helpers import parse_config, run_closure_compiler, create_engine_file +from emscripten_helpers import run_closure_compiler, create_engine_file +from SCons.Util import WhereIs def is_active(): @@ -12,7 +13,7 @@ def get_name(): def can_build(): - return "EM_CONFIG" in os.environ or os.path.exists(os.path.expanduser("~/.emscripten")) + return WhereIs("emcc") is not None def get_opts(): @@ -100,9 +101,6 @@ def configure(env): # Closure compiler extern and support for ecmascript specs (const, let, etc). env["ENV"]["EMCC_CLOSURE_ARGS"] = "--language_in ECMASCRIPT6" - em_config = parse_config() - env.PrependENVPath("PATH", em_config["EMCC_ROOT"]) - env["CC"] = "emcc" env["CXX"] = "em++" env["LINK"] = "emcc" @@ -137,8 +135,9 @@ def configure(env): env.Append(CPPDEFINES=["PTHREAD_NO_RENAME"]) env.Append(CCFLAGS=["-s", "USE_PTHREADS=1"]) env.Append(LINKFLAGS=["-s", "USE_PTHREADS=1"]) - env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=4"]) + env.Append(LINKFLAGS=["-s", "PTHREAD_POOL_SIZE=8"]) env.Append(LINKFLAGS=["-s", "WASM_MEM_MAX=2048MB"]) + env.extra_suffix = ".threads" + env.extra_suffix else: env.Append(CPPDEFINES=["NO_THREADS"]) diff --git a/platform/javascript/display_server_javascript.cpp b/platform/javascript/display_server_javascript.cpp index 2f0a2faa83..8dc33bdf64 100644 --- a/platform/javascript/display_server_javascript.cpp +++ b/platform/javascript/display_server_javascript.cpp @@ -75,7 +75,7 @@ bool DisplayServerJavaScript::check_size_force_redraw() { if (last_width != canvas_width || last_height != canvas_height) { last_width = canvas_width; last_height = canvas_height; - // Update the framebuffer size and for redraw. + // Update the framebuffer size for redraw. emscripten_set_canvas_element_size(DisplayServerJavaScript::canvas_id, canvas_width, canvas_height); return true; } @@ -829,6 +829,19 @@ DisplayServer *DisplayServerJavaScript::create_func(const String &p_rendering_dr } DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error) { + r_error = OK; // Always succeeds for now. + + /* clang-format off */ + swap_cancel_ok = EM_ASM_INT({ + const win = (['Windows', 'Win64', 'Win32', 'WinCE']); + const plat = navigator.platform || ""; + if (win.indexOf(plat) !== -1) { + return 1; + } + return 0; + }) == 1; + /* clang-format on */ + RasterizerDummy::make_current(); // TODO GLES2 in Godot 4.0... or webgpu? #if 0 EmscriptenWebGLContextAttributes attributes; @@ -879,15 +892,18 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive #define SET_EM_CALLBACK(target, ev, cb) \ result = emscripten_set_##ev##_callback(target, nullptr, true, &cb); \ EM_CHECK(ev) +#define SET_EM_WINDOW_CALLBACK(ev, cb) \ + result = emscripten_set_##ev##_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, &cb); \ + EM_CHECK(ev) #define SET_EM_CALLBACK_NOTARGET(ev, cb) \ result = emscripten_set_##ev##_callback(nullptr, true, &cb); \ EM_CHECK(ev) // These callbacks from Emscripten's html5.h suffice to access most // JavaScript APIs. For APIs that are not (sufficiently) exposed, EM_ASM // is used below. - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mousemove, mousemove_callback) SET_EM_CALLBACK(canvas_id, mousedown, mouse_button_callback) - SET_EM_CALLBACK(EMSCRIPTEN_EVENT_TARGET_WINDOW, mouseup, mouse_button_callback) + SET_EM_WINDOW_CALLBACK(mousemove, mousemove_callback) + SET_EM_WINDOW_CALLBACK(mouseup, mouse_button_callback) SET_EM_CALLBACK(canvas_id, wheel, wheel_callback) SET_EM_CALLBACK(canvas_id, touchstart, touch_press_callback) SET_EM_CALLBACK(canvas_id, touchmove, touchmove_callback) @@ -905,27 +921,25 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive /* clang-format off */ EM_ASM_ARGS({ - Module.listeners = {}; + // Bind native event listeners. + // Module.listeners, and Module.drop_handler are defined in native/utils.js const canvas = Module['canvas']; const send_window_event = cwrap('send_window_event', null, ['number']); const notifications = arguments; (['mouseover', 'mouseleave', 'focus', 'blur']).forEach(function(event, index) { - Module.listeners[event] = send_window_event.bind(null, notifications[index]); - canvas.addEventListener(event, Module.listeners[event]); + Module.listeners.add(canvas, event, send_window_event.bind(null, notifications[index]), true); }); // Clipboard const update_clipboard = cwrap('update_clipboard', null, ['string']); - Module.listeners['paste'] = function(evt) { + Module.listeners.add(window, 'paste', function(evt) { update_clipboard(evt.clipboardData.getData('text')); - }; - window.addEventListener('paste', Module.listeners['paste'], false); - Module.listeners['dragover'] = function(ev) { + }, false); + // Drag an drop + Module.listeners.add(canvas, 'dragover', function(ev) { // Prevent default behavior (which would try to open the file(s)) ev.preventDefault(); - }; - Module.listeners['drop'] = Module.drop_handler; // Defined in native/utils.js - canvas.addEventListener('dragover', Module.listeners['dragover'], false); - canvas.addEventListener('drop', Module.listeners['drop'], false); + }, false); + Module.listeners.add(canvas, 'drop', Module.drop_handler, false); }, WINDOW_EVENT_MOUSE_ENTER, WINDOW_EVENT_MOUSE_EXIT, @@ -939,14 +953,7 @@ DisplayServerJavaScript::DisplayServerJavaScript(const String &p_rendering_drive DisplayServerJavaScript::~DisplayServerJavaScript() { EM_ASM({ - Object.entries(Module.listeners).forEach(function(kv) { - if (kv[0] == 'paste') { - window.removeEventListener(kv[0], kv[1], true); - } else { - Module['canvas'].removeEventListener(kv[0], kv[1]); - } - }); - Module.listeners = {}; + Module.listeners.clear(); }); //emscripten_webgl_commit_frame(); //emscripten_webgl_destroy_context(webgl_ctx); @@ -1096,7 +1103,11 @@ Size2i DisplayServerJavaScript::window_get_min_size(WindowID p_window) const { void DisplayServerJavaScript::window_set_size(const Size2i p_size, WindowID p_window) { last_width = p_size.x; last_height = p_size.y; - emscripten_set_canvas_element_size(canvas_id, p_size.x, p_size.y); + double scale = EM_ASM_DOUBLE({ + return window.devicePixelRatio || 1; + }); + emscripten_set_canvas_element_size(canvas_id, p_size.x * scale, p_size.y * scale); + emscripten_set_element_css_size(canvas_id, p_size.x, p_size.y); } Size2i DisplayServerJavaScript::window_get_size(WindowID p_window) const { @@ -1181,6 +1192,10 @@ int DisplayServerJavaScript::get_current_video_driver() const { return 1; } +bool DisplayServerJavaScript::get_swap_cancel_ok() { + return swap_cancel_ok; +} + void DisplayServerJavaScript::swap_buffers() { //emscripten_webgl_commit_frame(); } diff --git a/platform/javascript/display_server_javascript.h b/platform/javascript/display_server_javascript.h index b149665d67..d7116be36f 100644 --- a/platform/javascript/display_server_javascript.h +++ b/platform/javascript/display_server_javascript.h @@ -56,6 +56,8 @@ class DisplayServerJavaScript : public DisplayServer { int last_width = 0; int last_height = 0; + bool swap_cancel_ok = false; + // utilities static Point2 compute_position_in_canvas(int p_x, int p_y); static void focus_canvas(); @@ -91,7 +93,7 @@ class DisplayServerJavaScript : public DisplayServer { static void _dispatch_input_event(const Ref<InputEvent> &p_event); protected: - virtual int get_current_video_driver() const; + int get_current_video_driver() const; public: // Override return type to make writing static callbacks less tedious. @@ -111,91 +113,92 @@ public: bool check_size_force_redraw(); // from DisplayServer - virtual void alert(const String &p_alert, const String &p_title = "ALERT!"); - virtual bool has_feature(Feature p_feature) const; - virtual String get_name() const; + void alert(const String &p_alert, const String &p_title = "ALERT!") override; + bool has_feature(Feature p_feature) const override; + String get_name() const override; // cursor - virtual void cursor_set_shape(CursorShape p_shape); - virtual CursorShape cursor_get_shape() const; - virtual void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()); + void cursor_set_shape(CursorShape p_shape) override; + CursorShape cursor_get_shape() const override; + void cursor_set_custom_image(const RES &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override; // mouse - virtual void mouse_set_mode(MouseMode p_mode); - virtual MouseMode mouse_get_mode() const; + void mouse_set_mode(MouseMode p_mode) override; + MouseMode mouse_get_mode() const override; // touch - virtual bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + bool screen_is_touchscreen(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; // clipboard - virtual void clipboard_set(const String &p_text); - virtual String clipboard_get() const; + void clipboard_set(const String &p_text) override; + String clipboard_get() const override; // screen - virtual int get_screen_count() const; - virtual Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const; - virtual Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const; - virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const; - virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + int get_screen_count() const override; + Point2i screen_get_position(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + Size2i screen_get_size(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; + int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const override; // windows - virtual Vector<DisplayServer::WindowID> get_window_list() const; - virtual WindowID get_window_at_screen_position(const Point2i &p_position) const; + Vector<DisplayServer::WindowID> get_window_list() const override; + 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); - virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const; + void window_attach_instance_id(ObjectID p_instance, WindowID p_window = MAIN_WINDOW_ID) override; + ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + 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); - virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); - virtual void window_set_input_text_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); + void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; + 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); + void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; - virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID); + 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; - virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID); + int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const override; + 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; - virtual void window_set_position(const Point2i &p_position, WindowID p_window = MAIN_WINDOW_ID); + Point2i window_get_position(WindowID p_window = MAIN_WINDOW_ID) const override; + 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); + 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); - virtual Size2i window_get_max_size(WindowID p_window = MAIN_WINDOW_ID) const; + void window_set_max_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + 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); - virtual Size2i window_get_min_size(WindowID p_window = MAIN_WINDOW_ID) const; + void window_set_min_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + 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); - virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const; - virtual Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const; // FIXME: Find clearer name for this. + void window_set_size(const Size2i p_size, WindowID p_window = MAIN_WINDOW_ID) override; + Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override; + Size2i window_get_real_size(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID); - virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const; + void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override; + 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; + 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); - virtual bool window_get_flag(WindowFlags p_flag, WindowID p_window = MAIN_WINDOW_ID) const; + void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) override; + 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); - virtual void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID); + void window_request_attention(WindowID p_window = MAIN_WINDOW_ID) override; + void window_move_to_foreground(WindowID p_window = MAIN_WINDOW_ID) override; - virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const; + bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const override; - virtual bool can_any_window_draw() const; + bool can_any_window_draw() const override; // events - virtual void process_events(); + void process_events() override; // icon - virtual void set_icon(const Ref<Image> &p_icon); + void set_icon(const Ref<Image> &p_icon) override; // others - virtual void swap_buffers(); + bool get_swap_cancel_ok() override; + void swap_buffers() override; static void register_javascript_driver(); DisplayServerJavaScript(const String &p_rendering_driver, WindowMode p_mode, uint32_t p_flags, const Vector2i &p_resolution, Error &r_error); diff --git a/platform/javascript/emscripten_helpers.py b/platform/javascript/emscripten_helpers.py index a55c9d3f48..f6db10fbbd 100644 --- a/platform/javascript/emscripten_helpers.py +++ b/platform/javascript/emscripten_helpers.py @@ -1,28 +1,11 @@ import os - -def parse_config(): - em_config_file = os.getenv("EM_CONFIG") or os.path.expanduser("~/.emscripten") - if not os.path.exists(em_config_file): - raise RuntimeError("Emscripten configuration file '%s' does not exist" % em_config_file) - - normalized = {} - em_config = {} - with open(em_config_file) as f: - try: - # Emscripten configuration file is a Python file with simple assignments. - exec(f.read(), em_config) - except StandardError as e: - raise RuntimeError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e)) - normalized["EMCC_ROOT"] = em_config.get("EMSCRIPTEN_ROOT") - normalized["NODE_JS"] = em_config.get("NODE_JS") - normalized["CLOSURE_BIN"] = os.path.join(normalized["EMCC_ROOT"], "node_modules", ".bin", "google-closure-compiler") - return normalized +from SCons.Util import WhereIs def run_closure_compiler(target, source, env, for_signature): - cfg = parse_config() - cmd = [cfg["NODE_JS"], cfg["CLOSURE_BIN"]] + closure_bin = os.path.join(os.path.dirname(WhereIs("emcc")), "node_modules", ".bin", "google-closure-compiler") + cmd = [WhereIs("node"), closure_bin] cmd.extend(["--compilation_level", "ADVANCED_OPTIMIZATIONS"]) for f in env["JSEXTERNS"]: cmd.extend(["--externs", f.get_abspath()]) diff --git a/platform/javascript/engine/engine.js b/platform/javascript/engine/engine.js index d709422abb..05a11701c0 100644 --- a/platform/javascript/engine/engine.js +++ b/platform/javascript/engine/engine.js @@ -33,6 +33,7 @@ Function('return this')()['Engine'] = (function() { this.resizeCanvasOnStart = false; this.onExecute = null; this.onExit = null; + this.persistentPaths = []; }; Engine.prototype.init = /** @param {string=} basePath */ function(basePath) { @@ -56,12 +57,14 @@ Function('return this')()['Engine'] = (function() { config['locateFile'] = Utils.createLocateRewrite(loadPath); config['instantiateWasm'] = Utils.createInstantiatePromise(loadPromise); Godot(config).then(function(module) { - me.rtenv = module; - if (unloadAfterInit) { - unload(); - } - resolve(); - config = null; + module['initFS'](me.persistentPaths).then(function(fs_err) { + me.rtenv = module; + if (unloadAfterInit) { + unload(); + } + resolve(); + config = null; + }); }); }); return initPromise; @@ -118,10 +121,11 @@ Function('return this')()['Engine'] = (function() { me.rtenv['noExitRuntime'] = true; me.rtenv['onExecute'] = me.onExecute; me.rtenv['onExit'] = function(code) { + me.rtenv['deinitFS'](); if (me.onExit) me.onExit(code); me.rtenv = null; - } + }; return new Promise(function(resolve, reject) { preloader.preloadedFiles.forEach(function(file) { me.rtenv['copyToFS'](file.path, file.buffer); @@ -207,18 +211,28 @@ Function('return this')()['Engine'] = (function() { if (this.rtenv) this.rtenv.onExecute = onExecute; this.onExecute = onExecute; - } + }; Engine.prototype.setOnExit = function(onExit) { this.onExit = onExit; - } + }; Engine.prototype.copyToFS = function(path, buffer) { if (this.rtenv == null) { throw new Error("Engine must be inited before copying files"); } this.rtenv['copyToFS'](path, buffer); - } + }; + + Engine.prototype.setPersistentPaths = function(persistentPaths) { + this.persistentPaths = persistentPaths; + }; + + Engine.prototype.requestQuit = function() { + if (this.rtenv) { + this.rtenv['request_quit'](); + } + }; // Closure compiler exported engine methods. /** @export */ @@ -241,5 +255,7 @@ Function('return this')()['Engine'] = (function() { Engine.prototype['setOnExecute'] = Engine.prototype.setOnExecute; Engine.prototype['setOnExit'] = Engine.prototype.setOnExit; Engine.prototype['copyToFS'] = Engine.prototype.copyToFS; + Engine.prototype['setPersistentPaths'] = Engine.prototype.setPersistentPaths; + Engine.prototype['requestQuit'] = Engine.prototype.requestQuit; return Engine; })(); diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index 6a3a977cfb..a83ff44d20 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -124,6 +124,9 @@ public: String s = "HTTP/1.1 200 OK\r\n"; s += "Connection: Close\r\n"; s += "Content-Type: " + ctype + "\r\n"; + s += "Access-Control-Allow-Origin: *\r\n"; + s += "Cross-Origin-Opener-Policy: same-origin\r\n"; + s += "Cross-Origin-Embedder-Policy: require-corp\r\n"; s += "\r\n"; CharString cs = s.utf8(); Error err = connection->put_data((const uint8_t *)cs.get_data(), cs.size() - 1); @@ -258,6 +261,7 @@ void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Re current_line = current_line.replace("$GODOT_BASENAME", p_name); current_line = current_line.replace("$GODOT_PROJECT_NAME", ProjectSettings::get_singleton()->get_setting("application/config/name")); current_line = current_line.replace("$GODOT_HEAD_INCLUDE", p_preset->get("html/head_include")); + current_line = current_line.replace("$GODOT_FULL_WINDOW", p_preset->get("html/full_window_size") ? "true" : "false"); current_line = current_line.replace("$GODOT_DEBUG_ENABLED", p_debug ? "true" : "false"); current_line = current_line.replace("$GODOT_ARGS", flags_json); str_export += current_line + "\n"; @@ -291,6 +295,7 @@ void EditorExportPlatformJavaScript::get_export_options(List<ExportOption> *r_op r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/custom_html_shell", PROPERTY_HINT_FILE, "*.html"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "html/head_include", PROPERTY_HINT_MULTILINE_TEXT), "")); + r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/full_window_size"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); } diff --git a/platform/javascript/godot_audio.h b/platform/javascript/godot_audio.h new file mode 100644 index 0000000000..f7f26e5262 --- /dev/null +++ b/platform/javascript/godot_audio.h @@ -0,0 +1,58 @@ +/*************************************************************************/ +/* godot_audio.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#ifndef GODOT_AUDIO_H +#define GODOT_AUDIO_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "stddef.h" + +extern int godot_audio_is_available(); + +extern int godot_audio_init(int p_mix_rate, int p_latency); +extern int godot_audio_create_processor(int p_buffer_length, int p_channel_count); + +extern void godot_audio_start(float *r_buffer_ptr); +extern void godot_audio_resume(); +extern void godot_audio_finish_async(); + +extern float godot_audio_get_latency(); + +extern void godot_audio_capture_start(); +extern void godot_audio_capture_stop(); + +#ifdef __cplusplus +} +#endif + +#endif /* GODOT_AUDIO_H */ diff --git a/platform/javascript/javascript_main.cpp b/platform/javascript/javascript_main.cpp index 99672745e7..01722c4bc8 100644 --- a/platform/javascript/javascript_main.cpp +++ b/platform/javascript/javascript_main.cpp @@ -34,10 +34,22 @@ #include "platform/javascript/os_javascript.h" #include <emscripten/emscripten.h> +#include <stdlib.h> static OS_JavaScript *os = nullptr; static uint64_t target_ticks = 0; +extern "C" EMSCRIPTEN_KEEPALIVE void _request_quit_callback(char *p_filev[], int p_filec) { + DisplayServerJavaScript *ds = DisplayServerJavaScript::get_singleton(); + if (ds) { + Variant event = int(DisplayServer::WINDOW_EVENT_CLOSE_REQUEST); + Variant *eventp = &event; + Variant ret; + Callable::CallError ce; + ds->window_event_callback.call((const Variant **)&eventp, 1, ret, ce); + } +} + void exit_callback() { emscripten_cancel_main_loop(); // After this, we can exit! Main::cleanup(); @@ -76,12 +88,27 @@ void main_loop_callback() { /* clang-format on */ os->get_main_loop()->finish(); os->finalize_async(); // Will add all the async finish functions. + /* clang-format off */ EM_ASM({ Promise.all(Module.async_finish).then(function() { Module.async_finish = []; + return new Promise(function(accept, reject) { + if (!Module.idbfs) { + accept(); + return; + } + FS.syncfs(function(error) { + if (error) { + err('Failed to save IDB file system: ' + error.message); + } + accept(); + }); + }); + }).then(function() { ccall("cleanup_after_sync", null, []); }); }); + /* clang-format on */ } } @@ -89,31 +116,8 @@ extern "C" EMSCRIPTEN_KEEPALIVE void cleanup_after_sync() { emscripten_set_main_loop(exit_callback, -1, false); } -extern "C" EMSCRIPTEN_KEEPALIVE void main_after_fs_sync(char *p_idbfs_err) { - String idbfs_err = String::utf8(p_idbfs_err); - if (!idbfs_err.empty()) { - print_line("IndexedDB not available: " + idbfs_err); - } - os->set_idb_available(idbfs_err.empty()); - // TODO: Check error return value. - Main::setup2(); // Manual second phase. - // Ease up compatibility. - ResourceLoader::set_abort_on_missing_resources(false); - Main::start(); - os->get_main_loop()->init(); - // Immediately run the first iteration. - // We are inside an animation frame, we want to immediately draw on the newly setup canvas. - main_loop_callback(); - emscripten_resume_main_loop(); -} - +/// When calling main, it is assumed FS is setup and synced. int main(int argc, char *argv[]) { - // Create and mount userfs immediately. - EM_ASM({ - FS.mkdir('/userfs'); - FS.mount(IDBFS, {}, '/userfs'); - }); - // Configure locale. char locale_ptr[16]; /* clang-format off */ @@ -131,22 +135,30 @@ int main(int argc, char *argv[]) { /* clang-format on */ os = new OS_JavaScript(); - Main::setup(argv[0], argc - 1, &argv[1], false); - emscripten_set_main_loop(main_loop_callback, -1, false); - emscripten_pause_main_loop(); // Will need to wait for FS sync. + os->set_idb_available((bool)EM_ASM_INT({ return Module.idbfs })); + + // We must override main when testing is enabled + TEST_MAIN_OVERRIDE + + Main::setup(argv[0], argc - 1, &argv[1]); + + // Ease up compatibility. + ResourceLoader::set_abort_on_missing_resources(false); - // Sync from persistent state into memory and then - // run the 'main_after_fs_sync' function. + Main::start(); + os->get_main_loop()->init(); + // Expose method for requesting quit. /* clang-format off */ EM_ASM({ - FS.syncfs(true, function(err) { - requestAnimationFrame(function() { - ccall('main_after_fs_sync', null, ['string'], [err ? err.message : ""]); - }); - }); + Module['request_quit'] = function() { + ccall("_request_quit_callback", null, []); + }; }); /* clang-format on */ + emscripten_set_main_loop(main_loop_callback, -1, false); + // Immediately run the first iteration. + // We are inside an animation frame, we want to immediately draw on the newly setup canvas. + main_loop_callback(); return 0; - // Continued async in main_after_fs_sync() from the syncfs() callback. } diff --git a/platform/javascript/native/library_godot_audio.js b/platform/javascript/native/library_godot_audio.js new file mode 100644 index 0000000000..d300280ccd --- /dev/null +++ b/platform/javascript/native/library_godot_audio.js @@ -0,0 +1,173 @@ +/*************************************************************************/ +/* library_godot_audio.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ +var GodotAudio = { + + $GodotAudio: { + + ctx: null, + input: null, + script: null, + }, + + godot_audio_is_available__proxy: 'sync', + godot_audio_is_available: function () { + if (!(window.AudioContext || window.webkitAudioContext)) { + return 0; + } + return 1; + }, + + godot_audio_init: function(mix_rate, latency) { + GodotAudio.ctx = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: mix_rate, + latencyHint: latency + }); + return GodotAudio.ctx.destination.channelCount; + }, + + godot_audio_create_processor: function(buffer_length, channel_count) { + GodotAudio.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); + GodotAudio.script.connect(GodotAudio.ctx.destination); + return GodotAudio.script.bufferSize; + }, + + godot_audio_start: function(buffer_ptr) { + var audioDriverProcessStart = cwrap('audio_driver_process_start'); + var audioDriverProcessEnd = cwrap('audio_driver_process_end'); + var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']); + GodotAudio.script.onaudioprocess = function(audioProcessingEvent) { + audioDriverProcessStart(); + + var input = audioProcessingEvent.inputBuffer; + var output = audioProcessingEvent.outputBuffer; + var internalBuffer = HEAPF32.subarray( + buffer_ptr / HEAPF32.BYTES_PER_ELEMENT, + buffer_ptr / HEAPF32.BYTES_PER_ELEMENT + output.length * output.numberOfChannels); + for (var channel = 0; channel < output.numberOfChannels; channel++) { + var outputData = output.getChannelData(channel); + // Loop through samples. + for (var sample = 0; sample < outputData.length; sample++) { + outputData[sample] = internalBuffer[sample * output.numberOfChannels + channel]; + } + } + + if (GodotAudio.input) { + var inputDataL = input.getChannelData(0); + var inputDataR = input.getChannelData(1); + for (var i = 0; i < inputDataL.length; i++) { + audioDriverProcessCapture(inputDataL[i]); + audioDriverProcessCapture(inputDataR[i]); + } + } + audioDriverProcessEnd(); + }; + }, + + godot_audio_resume: function() { + if (GodotAudio.ctx && GodotAudio.ctx.state != 'running') { + GodotAudio.ctx.resume(); + } + }, + + godot_audio_finish_async: function() { + Module.async_finish.push(new Promise(function(accept, reject) { + if (!GodotAudio.ctx) { + setTimeout(accept, 0); + } else { + if (GodotAudio.script) { + GodotAudio.script.disconnect(); + GodotAudio.script = null; + } + if (GodotAudio.input) { + GodotAudio.input.disconnect(); + GodotAudio.input = null; + } + GodotAudio.ctx.close().then(function() { + accept(); + }).catch(function(e) { + accept(); + }); + GodotAudio.ctx = null; + } + })); + }, + + godot_audio_get_latency__proxy: 'sync', + godot_audio_get_latency: function() { + var latency = 0; + if (GodotAudio.ctx) { + if (GodotAudio.ctx.baseLatency) { + latency += GodotAudio.ctx.baseLatency; + } + if (GodotAudio.ctx.outputLatency) { + latency += GodotAudio.ctx.outputLatency; + } + } + return latency; + }, + + godot_audio_capture_start__proxy: 'sync', + godot_audio_capture_start: function() { + if (GodotAudio.input) { + return; // Already started. + } + function gotMediaInput(stream) { + GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); + GodotAudio.input.connect(GodotAudio.script); + } + + function gotMediaInputError(e) { + out(e); + } + + if (navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({"audio": true}).then(gotMediaInput, gotMediaInputError); + } else { + if (!navigator.getUserMedia) + navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); + } + }, + + godot_audio_capture_stop__proxy: 'sync', + godot_audio_capture_stop: function() { + if (GodotAudio.input) { + const tracks = GodotAudio.input.mediaStream.getTracks(); + for (var i = 0; i < tracks.length; i++) { + tracks[i].stop(); + } + GodotAudio.input.disconnect(); + GodotAudio.input = null; + } + }, +}; + +autoAddDeps(GodotAudio, "$GodotAudio"); +mergeInto(LibraryManager.library, GodotAudio); diff --git a/platform/javascript/native/utils.js b/platform/javascript/native/utils.js index 95585d26ae..8d0beba454 100644 --- a/platform/javascript/native/utils.js +++ b/platform/javascript/native/utils.js @@ -28,6 +28,53 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +Module['initFS'] = function(persistentPaths) { + Module.mount_points = ['/userfs'].concat(persistentPaths); + + function createRecursive(dir) { + try { + FS.stat(dir); + } catch (e) { + if (e.errno !== ERRNO_CODES.ENOENT) { + throw e; + } + FS.mkdirTree(dir); + } + } + + Module.mount_points.forEach(function(path) { + createRecursive(path); + FS.mount(IDBFS, {}, path); + }); + return new Promise(function(resolve, reject) { + FS.syncfs(true, function(err) { + if (err) { + Module.mount_points = []; + Module.idbfs = false; + console.log("IndexedDB not available: " + err.message); + } else { + Module.idbfs = true; + } + resolve(err); + }); + }); +}; + +Module['deinitFS'] = function() { + Module.mount_points.forEach(function(path) { + try { + FS.unmount(path); + } catch (e) { + console.log("Already unmounted", e); + } + if (Module.idbfs && IDBFS.dbs[path]) { + IDBFS.dbs[path].close(); + delete IDBFS.dbs[path]; + } + }); + Module.mount_points = []; +}; + Module['copyToFS'] = function(path, buffer) { var p = path.lastIndexOf("/"); var dir = "/"; @@ -37,7 +84,7 @@ Module['copyToFS'] = function(path, buffer) { try { FS.stat(dir); } catch (e) { - if (e.errno !== ERRNO_CODES.ENOENT) { // 'ENOENT', see https://github.com/emscripten-core/emscripten/blob/master/system/lib/libc/musl/arch/emscripten/bits/errno.h + if (e.errno !== ERRNO_CODES.ENOENT) { throw e; } FS.mkdirTree(dir); @@ -202,3 +249,44 @@ Module.drop_handler = (function() { }); } })(); + +function EventHandlers() { + function Handler(target, event, method, capture) { + this.target = target; + this.event = event; + this.method = method; + this.capture = capture; + } + + var listeners = []; + + function has(target, event, method, capture) { + return listeners.findIndex(function(e) { + return e.target === target && e.event === event && e.method === method && e.capture == capture; + }) !== -1; + } + + this.add = function(target, event, method, capture) { + if (has(target, event, method, capture)) { + return; + } + listeners.push(new Handler(target, event, method, capture)); + target.addEventListener(event, method, capture); + }; + + this.remove = function(target, event, method, capture) { + if (!has(target, event, method, capture)) { + return; + } + target.removeEventListener(event, method, capture); + }; + + this.clear = function() { + listeners.forEach(function(h) { + h.target.removeEventListener(h.event, h.method, h.capture); + }); + listeners.length = 0; + }; +} + +Module.listeners = new EventHandlers(); diff --git a/platform/javascript/os_javascript.cpp b/platform/javascript/os_javascript.cpp index 1ff4304bcf..cf5751f384 100644 --- a/platform/javascript/os_javascript.cpp +++ b/platform/javascript/os_javascript.cpp @@ -46,24 +46,6 @@ #include <emscripten.h> #include <stdlib.h> -bool OS_JavaScript::has_touchscreen_ui_hint() const { - /* clang-format off */ - return EM_ASM_INT_V( - return 'ontouchstart' in window; - ); - /* clang-format on */ -} - -// Audio - -int OS_JavaScript::get_audio_driver_count() const { - return 1; -} - -const char *OS_JavaScript::get_audio_driver_name(int p_driver) const { - return "JavaScript"; -} - // Lifecycle void OS_JavaScript::initialize() { OS_Unix::initialize_core(); @@ -90,27 +72,24 @@ MainLoop *OS_JavaScript::get_main_loop() const { return main_loop; } -void OS_JavaScript::main_loop_callback() { - get_singleton()->main_loop_iterate(); +extern "C" EMSCRIPTEN_KEEPALIVE void _idb_synced() { + OS_JavaScript::get_singleton()->idb_is_syncing = false; } bool OS_JavaScript::main_loop_iterate() { - if (is_userfs_persistent() && sync_wait_time >= 0) { - int64_t current_time = get_ticks_msec(); - int64_t elapsed_time = current_time - last_sync_check_time; - last_sync_check_time = current_time; - - sync_wait_time -= elapsed_time; - - if (sync_wait_time < 0) { - /* clang-format off */ - EM_ASM( - FS.syncfs(function(error) { - if (error) { err('Failed to save IDB file system: ' + error.message); } - }); - ); - /* clang-format on */ - } + if (is_userfs_persistent() && idb_needs_sync && !idb_is_syncing) { + idb_is_syncing = true; + idb_needs_sync = false; + /* clang-format off */ + EM_ASM({ + FS.syncfs(function(error) { + if (error) { + err('Failed to save IDB file system: ' + error.message); + } + ccall("_idb_synced", 'void', [], []); + }); + }); + /* clang-format on */ } DisplayServer::get_singleton()->process_events(); @@ -201,10 +180,6 @@ String OS_JavaScript::get_name() const { return "HTML5"; } -bool OS_JavaScript::can_draw() const { - return true; // Always? -} - String OS_JavaScript::get_user_data_dir() const { return "/userfs"; }; @@ -222,11 +197,17 @@ String OS_JavaScript::get_data_path() const { } void OS_JavaScript::file_access_close_callback(const String &p_file, int p_flags) { - OS_JavaScript *os = get_singleton(); - if (os->is_userfs_persistent() && p_file.begins_with("/userfs") && p_flags & FileAccess::WRITE) { - os->last_sync_check_time = OS::get_singleton()->get_ticks_msec(); - // Wait five seconds in case more files are about to be closed. - os->sync_wait_time = 5000; + OS_JavaScript *os = OS_JavaScript::get_singleton(); + if (!(os->is_userfs_persistent() && (p_flags & FileAccess::WRITE))) { + return; // FS persistence is not working or we are not writing. + } + bool is_file_persistent = p_file.begins_with("/userfs"); +#ifdef TOOLS_ENABLED + // Hack for editor persistence (can we track). + is_file_persistent = is_file_persistent || p_file.begins_with("/home/web_user/"); +#endif + if (is_file_persistent) { + os->idb_needs_sync = true; } } diff --git a/platform/javascript/os_javascript.h b/platform/javascript/os_javascript.h index 22234f9355..85551d708b 100644 --- a/platform/javascript/os_javascript.h +++ b/platform/javascript/os_javascript.h @@ -44,57 +44,52 @@ class OS_JavaScript : public OS_Unix { bool finalizing = false; bool idb_available = false; - int64_t sync_wait_time = -1; - int64_t last_sync_check_time = -1; + bool idb_needs_sync = false; static void main_loop_callback(); static void file_access_close_callback(const String &p_file, int p_flags); protected: - virtual void initialize(); + void initialize() override; - virtual void set_main_loop(MainLoop *p_main_loop); - virtual void delete_main_loop(); + void set_main_loop(MainLoop *p_main_loop) override; + void delete_main_loop() override; - virtual void finalize(); + void finalize() override; - virtual bool _check_internal_feature_support(const String &p_feature); + bool _check_internal_feature_support(const String &p_feature) override; public: + bool idb_is_syncing = false; + // Override return type to make writing static callbacks less tedious. static OS_JavaScript *get_singleton(); - virtual void initialize_joypads(); - - virtual bool has_touchscreen_ui_hint() const; - - virtual int get_audio_driver_count() const; - virtual const char *get_audio_driver_name(int p_driver) const; + void initialize_joypads() override; - virtual MainLoop *get_main_loop() const; + MainLoop *get_main_loop() const override; void finalize_async(); bool main_loop_iterate(); - virtual Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr); - virtual Error kill(const ProcessID &p_pid); - virtual int get_process_id() const; + Error execute(const String &p_path, const List<String> &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr) override; + Error kill(const ProcessID &p_pid) override; + int get_process_id() const override; - String get_executable_path() const; - virtual Error shell_open(String p_uri); - virtual String get_name() const; + String get_executable_path() const override; + Error shell_open(String p_uri) override; + String get_name() const override; // Override default OS implementation which would block the main thread with delay_usec. // Implemented in javascript_main.cpp loop callback instead. - virtual void add_frame_delay(bool p_can_draw) {} - virtual bool can_draw() const; + void add_frame_delay(bool p_can_draw) override {} - virtual String get_cache_path() const; - virtual String get_config_path() const; - virtual String get_data_path() const; - virtual String get_user_data_dir() const; + String get_cache_path() const override; + String get_config_path() const override; + String get_data_path() const override; + String get_user_data_dir() const override; void set_idb_available(bool p_idb_available); - virtual bool is_userfs_persistent() const; + bool is_userfs_persistent() const override; void resume_audio(); bool is_finalizing() { return finalizing; } diff --git a/platform/linuxbsd/crash_handler_linuxbsd.cpp b/platform/linuxbsd/crash_handler_linuxbsd.cpp index b3553e961a..e2b88b7704 100644 --- a/platform/linuxbsd/crash_handler_linuxbsd.cpp +++ b/platform/linuxbsd/crash_handler_linuxbsd.cpp @@ -67,7 +67,7 @@ static void handle_crash(int sig) { OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); } - fprintf(stderr, "Dumping the backtrace. %ls\n", msg.c_str()); + fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); char **strings = backtrace_symbols(bt_buffer, size); if (strings) { for (size_t i = 1; i < size; i++) { @@ -109,7 +109,7 @@ static void handle_crash(int sig) { output.erase(output.length() - 1, 1); } - fprintf(stderr, "[%ld] %s (%ls)\n", (long int)i, fname, output.c_str()); + fprintf(stderr, "[%ld] %s (%s)\n", (long int)i, fname, output.utf8().get_data()); } free(strings); diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 3eb4c44bc1..f5e2c72bbc 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -35,6 +35,11 @@ def can_build(): print("xinerama not found.. x11 disabled.") return False + x11_error = os.system("pkg-config xext --modversion > /dev/null ") + if x11_error: + print("xext not found.. x11 disabled.") + return False + x11_error = os.system("pkg-config xrandr --modversion > /dev/null ") if x11_error: print("xrandr not found.. x11 disabled.") @@ -194,6 +199,7 @@ def configure(env): env.ParseConfig("pkg-config x11 --cflags --libs") env.ParseConfig("pkg-config xcursor --cflags --libs") env.ParseConfig("pkg-config xinerama --cflags --libs") + env.ParseConfig("pkg-config xext --cflags --libs") env.ParseConfig("pkg-config xrandr --cflags --libs") env.ParseConfig("pkg-config xrender --cflags --libs") env.ParseConfig("pkg-config xi --cflags --libs") diff --git a/platform/linuxbsd/display_server_x11.cpp b/platform/linuxbsd/display_server_x11.cpp index 874a3a6392..176878bc12 100644 --- a/platform/linuxbsd/display_server_x11.cpp +++ b/platform/linuxbsd/display_server_x11.cpp @@ -39,10 +39,6 @@ #include "main/main.h" #include "scene/resources/texture.h" -#if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" -#endif - #if defined(VULKAN_ENABLED) #include "servers/rendering/rasterizer_rd/rasterizer_rd.h" #endif @@ -54,6 +50,7 @@ #include <X11/Xatom.h> #include <X11/Xutil.h> #include <X11/extensions/Xinerama.h> +#include <X11/extensions/shape.h> // ICCCM #define WM_NormalState 1L // window normal state @@ -87,6 +84,13 @@ #define VALUATOR_TILTX 3 #define VALUATOR_TILTY 4 +//#define DISPLAY_SERVER_X11_DEBUG_LOGS_ENABLED +#ifdef DISPLAY_SERVER_X11_DEBUG_LOGS_ENABLED +#define DEBUG_LOG_X11(...) printf(__VA_ARGS__) +#else +#define DEBUG_LOG_X11(...) +#endif + static const double abs_resolution_mult = 10000.0; static const double abs_resolution_range_mult = 10.0; @@ -318,23 +322,19 @@ bool DisplayServerX11::_refresh_device_info() { } void DisplayServerX11::_flush_mouse_motion() { - while (true) { - if (XPending(x11_display) > 0) { - XEvent event; - XPeekEvent(x11_display, &event); - - if (XGetEventData(x11_display, &event.xcookie) && event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) { - XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data; - - if (event_data->evtype == XI_RawMotion) { - XNextEvent(x11_display, &event); - } else { - break; - } - } else { - break; + // Block events polling while flushing motion events. + MutexLock mutex_lock(events_mutex); + + for (uint32_t event_index = 0; event_index < polled_events.size(); ++event_index) { + XEvent &event = polled_events[event_index]; + if (XGetEventData(x11_display, &event.xcookie) && event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) { + XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data; + if (event_data->evtype == XI_RawMotion) { + XFreeEventData(x11_display, &event.xcookie); + polled_events.remove(event_index--); + continue; } - } else { + XFreeEventData(x11_display, &event.xcookie); break; } } @@ -447,12 +447,25 @@ int DisplayServerX11::mouse_get_button_state() const { void DisplayServerX11::clipboard_set(const String &p_text) { _THREAD_SAFE_METHOD_ - internal_clipboard = p_text; + { + // The clipboard content can be accessed while polling for events. + MutexLock mutex_lock(events_mutex); + internal_clipboard = p_text; + } + XSetSelectionOwner(x11_display, XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window, CurrentTime); XSetSelectionOwner(x11_display, XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window, CurrentTime); } -static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x11_display, String p_internal_clipboard, Atom target) { +Bool DisplayServerX11::_predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg) { + if (event->type == SelectionNotify && event->xselection.requestor == *(Window *)arg) { + return True; + } else { + return False; + } +} + +String DisplayServerX11::_clipboard_get_impl(Atom p_source, Window x11_window, Atom target) const { String ret; Atom type; @@ -460,23 +473,27 @@ static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x int format, result; unsigned long len, bytes_left, dummy; unsigned char *data; - Window Sown = XGetSelectionOwner(x11_display, p_source); + Window selection_owner = XGetSelectionOwner(x11_display, p_source); - if (Sown == x11_window) { - return p_internal_clipboard; - }; + if (selection_owner == x11_window) { + return internal_clipboard; + } - if (Sown != None) { - XConvertSelection(x11_display, p_source, target, selection, - x11_window, CurrentTime); - XFlush(x11_display); - while (true) { + if (selection_owner != None) { + { + // Block events polling while processing selection events. + MutexLock mutex_lock(events_mutex); + + XConvertSelection(x11_display, p_source, target, selection, + x11_window, CurrentTime); + + XFlush(x11_display); + + // Blocking wait for predicate to be True + // and remove the event from the queue. XEvent event; - XNextEvent(x11_display, &event); - if (event.type == SelectionNotify && event.xselection.requestor == x11_window) { - break; - }; - }; + XIfEvent(x11_display, &event, _predicate_clipboard_selection, (XPointer)&x11_window); + } // // Do not get any data, see how much data is there @@ -510,14 +527,14 @@ static String _clipboard_get_impl(Atom p_source, Window x11_window, ::Display *x return ret; } -static String _clipboard_get(Atom p_source, Window x11_window, ::Display *x11_display, String p_internal_clipboard) { +String DisplayServerX11::_clipboard_get(Atom p_source, Window x11_window) const { String ret; Atom utf8_atom = XInternAtom(x11_display, "UTF8_STRING", True); if (utf8_atom != None) { - ret = _clipboard_get_impl(p_source, x11_window, x11_display, p_internal_clipboard, utf8_atom); + ret = _clipboard_get_impl(p_source, x11_window, utf8_atom); } - if (ret == "") { - ret = _clipboard_get_impl(p_source, x11_window, x11_display, p_internal_clipboard, XA_STRING); + if (ret.empty()) { + ret = _clipboard_get_impl(p_source, x11_window, XA_STRING); } return ret; } @@ -526,11 +543,11 @@ String DisplayServerX11::clipboard_get() const { _THREAD_SAFE_METHOD_ String ret; - ret = _clipboard_get(XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window, x11_display, internal_clipboard); + ret = _clipboard_get(XInternAtom(x11_display, "CLIPBOARD", 0), windows[MAIN_WINDOW_ID].x11_window); - if (ret == "") { - ret = _clipboard_get(XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window, x11_display, internal_clipboard); - }; + if (ret.empty()) { + ret = _clipboard_get(XA_PRIMARY, windows[MAIN_WINDOW_ID].x11_window); + } return ret; } @@ -685,6 +702,14 @@ DisplayServer::WindowID DisplayServerX11::create_sub_window(WindowMode p_mode, u return id; } +void DisplayServerX11::show_window(WindowID p_id) { + _THREAD_SAFE_METHOD_ + + WindowData &wd = windows[p_id]; + + XMapWindow(x11_display, wd.x11_window); +} + void DisplayServerX11::delete_sub_window(WindowID p_id) { _THREAD_SAFE_METHOD_ @@ -693,6 +718,8 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) { WindowData &wd = windows[p_id]; + DEBUG_LOG_X11("delete_sub_window: %lu (%u) \n", wd.x11_window, p_id); + while (wd.transient_children.size()) { window_set_transient(wd.transient_children.front()->get(), INVALID_WINDOW_ID); } @@ -710,6 +737,7 @@ void DisplayServerX11::delete_sub_window(WindowID p_id) { XDestroyWindow(x11_display, wd.x11_window); if (wd.xic) { XDestroyIC(wd.xic); + wd.xic = nullptr; } windows.erase(p_id); @@ -729,15 +757,31 @@ ObjectID DisplayServerX11::window_get_attached_instance_id(WindowID p_window) co } DisplayServerX11::WindowID DisplayServerX11::get_window_at_screen_position(const Point2i &p_position) const { -#warning This is an incorrect implementation, if windows overlap, it should return the topmost visible one or none if occluded by a foreign window - + WindowID found_window = INVALID_WINDOW_ID; + WindowID parent_window = INVALID_WINDOW_ID; + unsigned int focus_order = 0; for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - Rect2i win_rect = Rect2i(window_get_position(E->key()), window_get_size(E->key())); + const WindowData &wd = E->get(); + + // Discard windows with no focus. + if (wd.focus_order == 0) { + continue; + } + + // Find topmost window which contains the given position. + WindowID window_id = E->key(); + Rect2i win_rect = Rect2i(window_get_position(window_id), window_get_size(window_id)); if (win_rect.has_point(p_position)) { - return E->key(); + // For siblings, pick the window which was focused last. + if ((parent_window != wd.transient_parent) || (wd.focus_order > focus_order)) { + found_window = window_id; + parent_window = wd.transient_parent; + focus_order = wd.focus_order; + } } } - return INVALID_WINDOW_ID; + + return found_window; } void DisplayServerX11::window_set_title(const String &p_title, WindowID p_window) { @@ -753,6 +797,38 @@ void DisplayServerX11::window_set_title(const String &p_title, WindowID p_window XChangeProperty(x11_display, wd.x11_window, _net_wm_name, utf8_string, 8, PropModeReplace, (unsigned char *)p_title.utf8().get_data(), p_title.utf8().length()); } +void DisplayServerX11::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!windows.has(p_window)); + const WindowData &wd = windows[p_window]; + + int event_base, error_base; + const Bool ext_okay = XShapeQueryExtension(x11_display, &event_base, &error_base); + if (ext_okay) { + Region region; + if (p_region.size() == 0) { + region = XCreateRegion(); + XRectangle rect; + rect.x = 0; + rect.y = 0; + rect.width = window_get_real_size(p_window).x; + rect.height = window_get_real_size(p_window).y; + XUnionRectWithRegion(&rect, region, region); + } else { + XPoint *points = (XPoint *)memalloc(sizeof(XPoint) * p_region.size()); + for (int i = 0; i < p_region.size(); i++) { + points[i].x = p_region[i].x; + points[i].y = p_region[i].y; + } + region = XPolygonRegion(points, p_region.size(), EvenOddRule); + memfree(points); + } + XShapeCombineRegion(x11_display, wd.x11_window, ShapeInput, 0, 0, region, ShapeSet); + XDestroyRegion(region); + } +} + void DisplayServerX11::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -846,24 +922,34 @@ void DisplayServerX11::window_set_transient(WindowID p_window, WindowID p_parent ERR_FAIL_COND(!windows.has(p_window)); WindowData &wd_window = windows[p_window]; - ERR_FAIL_COND(wd_window.transient_parent == p_parent); + WindowID prev_parent = wd_window.transient_parent; + ERR_FAIL_COND(prev_parent == p_parent); ERR_FAIL_COND_MSG(wd_window.on_top, "Windows with the 'on top' can't become transient."); if (p_parent == INVALID_WINDOW_ID) { //remove transient - ERR_FAIL_COND(wd_window.transient_parent == INVALID_WINDOW_ID); - ERR_FAIL_COND(!windows.has(wd_window.transient_parent)); + ERR_FAIL_COND(prev_parent == INVALID_WINDOW_ID); + ERR_FAIL_COND(!windows.has(prev_parent)); - WindowData &wd_parent = windows[wd_window.transient_parent]; + WindowData &wd_parent = windows[prev_parent]; wd_window.transient_parent = INVALID_WINDOW_ID; wd_parent.transient_children.erase(p_window); XSetTransientForHint(x11_display, wd_window.x11_window, None); + + // Set focus to parent sub window to avoid losing all focus with nested menus. + // RevertToPointerRoot is used to make sure we don't lose all focus in case + // a subwindow and its parent are both destroyed. + if (wd_window.menu_type && !wd_window.no_focus) { + if (!wd_parent.no_focus) { + XSetInputFocus(x11_display, wd_parent.x11_window, RevertToPointerRoot, CurrentTime); + } + } } else { ERR_FAIL_COND(!windows.has(p_parent)); - ERR_FAIL_COND_MSG(wd_window.transient_parent != INVALID_WINDOW_ID, "Window already has a transient parent"); + ERR_FAIL_COND_MSG(prev_parent != INVALID_WINDOW_ID, "Window already has a transient parent"); WindowData &wd_parent = windows[p_parent]; wd_window.transient_parent = p_parent; @@ -873,6 +959,46 @@ void DisplayServerX11::window_set_transient(WindowID p_window, WindowID p_parent } } +// Helper method. Assumes that the window id has already been checked and exists. +void DisplayServerX11::_update_size_hints(WindowID p_window) { + WindowData &wd = windows[p_window]; + WindowMode window_mode = window_get_mode(p_window); + XSizeHints *xsh = XAllocSizeHints(); + + // Always set the position and size hints - they should be synchronized with the actual values after the window is mapped anyway + xsh->flags |= PPosition | PSize; + xsh->x = wd.position.x; + xsh->y = wd.position.y; + xsh->width = wd.size.width; + xsh->height = wd.size.height; + + if (window_mode == WINDOW_MODE_FULLSCREEN) { + // Do not set any other hints to prevent the window manager from ignoring the fullscreen flags + } else if (window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { + // If resizing is disabled, use the forced size + xsh->flags |= PMinSize | PMaxSize; + xsh->min_width = wd.size.x; + xsh->max_width = wd.size.x; + xsh->min_height = wd.size.y; + xsh->max_height = wd.size.y; + } else { + // Otherwise, just respect min_size and max_size + if (wd.min_size != Size2i()) { + xsh->flags |= PMinSize; + xsh->min_width = wd.min_size.x; + xsh->min_height = wd.min_size.y; + } + if (wd.max_size != Size2i()) { + xsh->flags |= PMaxSize; + xsh->max_width = wd.max_size.x; + xsh->max_height = wd.max_size.y; + } + } + + XSetWMNormalHints(x11_display, wd.x11_window, xsh); + XFree(xsh); +} + Point2i DisplayServerX11::window_get_position(WindowID p_window) const { _THREAD_SAFE_METHOD_ @@ -926,25 +1052,8 @@ void DisplayServerX11::window_set_max_size(const Size2i p_size, WindowID p_windo } wd.max_size = p_size; - if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { - XSizeHints *xsh; - xsh = XAllocSizeHints(); - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); - - XFlush(x11_display); - } + _update_size_hints(p_window); + XFlush(x11_display); } Size2i DisplayServerX11::window_get_max_size(WindowID p_window) const { @@ -968,25 +1077,8 @@ void DisplayServerX11::window_set_min_size(const Size2i p_size, WindowID p_windo } wd.min_size = p_size; - if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { - XSizeHints *xsh; - xsh = XAllocSizeHints(); - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); - - XFlush(x11_display); - } + _update_size_hints(p_window); + XFlush(x11_display); } Size2i DisplayServerX11::window_get_min_size(WindowID p_window) const { @@ -1019,37 +1111,15 @@ void DisplayServerX11::window_set_size(const Size2i p_size, WindowID p_window) { int old_w = xwa.width; int old_h = xwa.height; - // If window resizable is disabled we need to update the attributes first - XSizeHints *xsh; - xsh = XAllocSizeHints(); - if (!window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { - xsh->flags = PMinSize | PMaxSize; - xsh->min_width = size.x; - xsh->max_width = size.x; - xsh->min_height = size.y; - xsh->max_height = size.y; - } else { - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - } - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); + // Update our videomode width and height + wd.size = size; + + // Update the size hints first to make sure the window size can be set + _update_size_hints(p_window); // Resize the window XResizeWindow(x11_display, wd.x11_window, size.x, size.y); - // Update our videomode width and height - wd.size = size; - for (int timeout = 0; timeout < 50; ++timeout) { XSync(x11_display, False); XGetWindowAttributes(x11_display, wd.x11_window, &xwa); @@ -1205,14 +1275,9 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) { XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); } - if (p_enabled && window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { + if (p_enabled) { // Set the window as resizable to prevent window managers to ignore the fullscreen state flag. - XSizeHints *xsh; - - xsh = XAllocSizeHints(); - xsh->flags = 0L; - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); + _update_size_hints(p_window); } // Using EWMH -- Extended Window Manager Hints @@ -1240,30 +1305,7 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled) { if (!p_enabled) { // Reset the non-resizable flags if we un-set these before. - Size2i size = window_get_size(p_window); - XSizeHints *xsh; - xsh = XAllocSizeHints(); - if (window_get_flag(WINDOW_FLAG_RESIZE_DISABLED, p_window)) { - xsh->flags = PMinSize | PMaxSize; - xsh->min_width = size.x; - xsh->max_width = size.x; - xsh->min_height = size.y; - xsh->max_height = size.y; - } else { - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - } - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); + _update_size_hints(p_window); // put back or remove decorations according to the last set borderless state Hints hints; @@ -1321,13 +1363,13 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) { } break; case WINDOW_MODE_FULLSCREEN: { //Remove full-screen + wd.fullscreen = false; + _set_wm_fullscreen(p_window, false); //un-maximize required for always on top bool on_top = window_get_flag(WINDOW_FLAG_ALWAYS_ON_TOP, p_window); - wd.fullscreen = false; - window_set_position(wd.last_position_before_fs, p_window); if (on_top) { @@ -1373,15 +1415,16 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) { } break; case WINDOW_MODE_FULLSCREEN: { wd.last_position_before_fs = wd.position; + if (window_get_flag(WINDOW_FLAG_ALWAYS_ON_TOP, p_window)) { _set_wm_maximized(p_window, true); } - _set_wm_fullscreen(p_window, true); + wd.fullscreen = true; + _set_wm_fullscreen(p_window, true); } break; case WINDOW_MODE_MAXIMIZED: { _set_wm_maximized(p_window, true); - } break; } } @@ -1448,37 +1491,11 @@ void DisplayServerX11::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo switch (p_flag) { case WINDOW_FLAG_RESIZE_DISABLED: { - XSizeHints *xsh; - xsh = XAllocSizeHints(); - if (p_enabled) { - Size2i size = window_get_size(p_window); - - xsh->flags = PMinSize | PMaxSize; - xsh->min_width = size.x; - xsh->max_width = size.x; - xsh->min_height = size.y; - xsh->max_height = size.y; - } else { - xsh->flags = 0L; - if (wd.min_size != Size2i()) { - xsh->flags |= PMinSize; - xsh->min_width = wd.min_size.x; - xsh->min_height = wd.min_size.y; - } - if (wd.max_size != Size2i()) { - xsh->flags |= PMaxSize; - xsh->max_width = wd.max_size.x; - xsh->max_height = wd.max_size.y; - } - } - - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); - wd.resize_disabled = p_enabled; - XFlush(x11_display); + _update_size_hints(p_window); + XFlush(x11_display); } break; case WINDOW_FLAG_BORDERLESS: { Hints hints; @@ -1646,10 +1663,16 @@ void DisplayServerX11::window_set_ime_active(const bool p_active, WindowID p_win return; } + // Block events polling while changing input focus + // because it triggers some event polling internally. if (p_active) { - XSetICFocus(wd.xic); + { + MutexLock mutex_lock(events_mutex); + XSetICFocus(wd.xic); + } window_set_ime_position(wd.im_position, p_window); } else { + MutexLock mutex_lock(events_mutex); XUnsetICFocus(wd.xic); } } @@ -1670,7 +1693,14 @@ void DisplayServerX11::window_set_ime_position(const Point2i &p_pos, WindowID p_ spot.x = short(p_pos.x); spot.y = short(p_pos.y); XVaNestedList preedit_attr = XVaCreateNestedList(0, XNSpotLocation, &spot, nullptr); - XSetICValues(wd.xic, XNPreeditAttributes, preedit_attr, nullptr); + + { + // Block events polling during this call + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + XSetICValues(wd.xic, XNPreeditAttributes, preedit_attr, nullptr); + } + XFree(preedit_attr); } @@ -1990,7 +2020,7 @@ unsigned int DisplayServerX11::_get_mouse_button_state(unsigned int p_x11_button return last_button_state; } -void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, bool p_echo) { +void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo) { WindowData wd = windows[p_window]; // X11 functions don't know what const is XKeyEvent *xkeyevent = p_event; @@ -2127,7 +2157,7 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, /* Phase 4, determine if event must be filtered */ // This seems to be a side-effect of using XIM. - // XEventFilter looks like a core X11 function, + // XFilterEvent looks like a core X11 function, // but it's actually just used to see if we must // ignore a deadkey, or events XIM determines // must not reach the actual gui. @@ -2161,17 +2191,16 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, // Echo characters in X11 are a keyrelease and a keypress // one after the other with the (almot) same timestamp. - // To detect them, i use XPeekEvent and check that their - // difference in time is below a threshold. + // To detect them, i compare to the next event in list and + // check that their difference in time is below a threshold. if (xkeyevent->type != KeyPress) { p_echo = false; // make sure there are events pending, // so this call won't block. - if (XPending(x11_display) > 0) { - XEvent peek_event; - XPeekEvent(x11_display, &peek_event); + if (p_event_index + 1 < p_events.size()) { + XEvent &peek_event = p_events[p_event_index + 1]; // I'm using a threshold of 5 msecs, // since sometimes there seems to be a little @@ -2186,9 +2215,9 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, KeySym rk; XLookupString((XKeyEvent *)&peek_event, str, 256, &rk, nullptr); if (rk == keysym_keycode) { - XEvent event; - XNextEvent(x11_display, &event); //erase next event - _handle_key_event(p_window, (XKeyEvent *)&event, true); + // Consume to next event. + ++p_event_index; + _handle_key_event(p_window, (XKeyEvent *)&peek_event, p_events, p_event_index, true); return; //ignore current, echo next } } @@ -2243,6 +2272,66 @@ void DisplayServerX11::_handle_key_event(WindowID p_window, XKeyEvent *p_event, Input::get_singleton()->accumulate_input_event(k); } +void DisplayServerX11::_handle_selection_request_event(XSelectionRequestEvent *p_event) { + XEvent respond; + if (p_event->target == XInternAtom(x11_display, "UTF8_STRING", 0) || + p_event->target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) || + p_event->target == XInternAtom(x11_display, "TEXT", 0) || + p_event->target == XA_STRING || + p_event->target == XInternAtom(x11_display, "text/plain;charset=utf-8", 0) || + p_event->target == XInternAtom(x11_display, "text/plain", 0)) { + // Directly using internal clipboard because we know our window + // is the owner during a selection request. + CharString clip = internal_clipboard.utf8(); + XChangeProperty(x11_display, + p_event->requestor, + p_event->property, + p_event->target, + 8, + PropModeReplace, + (unsigned char *)clip.get_data(), + clip.length()); + respond.xselection.property = p_event->property; + } else if (p_event->target == XInternAtom(x11_display, "TARGETS", 0)) { + Atom data[7]; + data[0] = XInternAtom(x11_display, "TARGETS", 0); + data[1] = XInternAtom(x11_display, "UTF8_STRING", 0); + data[2] = XInternAtom(x11_display, "COMPOUND_TEXT", 0); + data[3] = XInternAtom(x11_display, "TEXT", 0); + data[4] = XA_STRING; + data[5] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0); + data[6] = XInternAtom(x11_display, "text/plain", 0); + + XChangeProperty(x11_display, + p_event->requestor, + p_event->property, + XA_ATOM, + 32, + PropModeReplace, + (unsigned char *)&data, + sizeof(data) / sizeof(data[0])); + respond.xselection.property = p_event->property; + + } else { + char *targetname = XGetAtomName(x11_display, p_event->target); + printf("No Target '%s'\n", targetname); + if (targetname) { + XFree(targetname); + } + respond.xselection.property = None; + } + + respond.xselection.type = SelectionNotify; + respond.xselection.display = p_event->display; + respond.xselection.requestor = p_event->requestor; + respond.xselection.selection = p_event->selection; + respond.xselection.target = p_event->target; + respond.xselection.time = p_event->time; + + XSendEvent(x11_display, p_event->requestor, True, NoEventMask, &respond); + XFlush(x11_display); +} + void DisplayServerX11::_xim_destroy_callback(::XIM im, ::XPointer client_data, ::XPointer call_data) { WARN_PRINT("Input method stopped"); @@ -2355,9 +2444,73 @@ void DisplayServerX11::_send_window_event(const WindowData &wd, WindowEvent p_ev } } +void DisplayServerX11::_poll_events_thread(void *ud) { + DisplayServerX11 *display_server = (DisplayServerX11 *)ud; + display_server->_poll_events(); +} + +Bool DisplayServerX11::_predicate_all_events(Display *display, XEvent *event, XPointer arg) { + // Just accept all events. + return True; +} + +void DisplayServerX11::_poll_events() { + int x11_fd = ConnectionNumber(x11_display); + fd_set in_fds; + + while (!events_thread_done) { + XFlush(x11_display); + + FD_ZERO(&in_fds); + FD_SET(x11_fd, &in_fds); + + struct timeval tv; + tv.tv_usec = 0; + tv.tv_sec = 1; + + // Wait for next event or timeout. + int num_ready_fds = select(x11_fd + 1, &in_fds, NULL, NULL, &tv); + if (num_ready_fds < 0) { + ERR_PRINT("_poll_events: select error: " + itos(errno)); + } + + // Process events from the queue. + { + MutexLock mutex_lock(events_mutex); + + // Non-blocking wait for next event + // and remove it from the queue. + XEvent ev; + while (XCheckIfEvent(x11_display, &ev, _predicate_all_events, nullptr)) { + // Check if the input manager wants to process the event. + if (XFilterEvent(&ev, None)) { + // Event has been filtered by the Input Manager, + // it has to be ignored and a new one will be received. + continue; + } + + // Handle selection request events directly in the event thread, because + // communication through the x server takes several events sent back and forth + // and we don't want to block other programs while processing only one each frame. + if (ev.type == SelectionRequest) { + _handle_selection_request_event(&(ev.xselectionrequest)); + continue; + } + + polled_events.push_back(ev); + } + } + } +} + void DisplayServerX11::process_events() { _THREAD_SAFE_METHOD_ +#ifdef DISPLAY_SERVER_X11_DEBUG_LOGS_ENABLED + static int frame = 0; + ++frame; +#endif + if (app_focused) { //verify that one of the windows has focus, else send focus out notification bool focus_found = false; @@ -2374,6 +2527,7 @@ void DisplayServerX11::process_events() { if (delta > 250) { //X11 can go between windows and have no focus for a while, when creating them or something else. Use this as safety to avoid unnecessary focus in/outs. if (OS::get_singleton()->get_main_loop()) { + DEBUG_LOG_X11("All focus lost, triggering NOTIFICATION_APPLICATION_FOCUS_OUT\n"); OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_APPLICATION_FOCUS_OUT); } app_focused = false; @@ -2392,9 +2546,16 @@ void DisplayServerX11::process_events() { xi.tilt = Vector2(); xi.pressure_supported = false; - while (XPending(x11_display) > 0) { - XEvent event; - XNextEvent(x11_display, &event); + LocalVector<XEvent> events; + { + // Block events polling while flushing events. + MutexLock mutex_lock(events_mutex); + events = polled_events; + polled_events.clear(); + } + + for (uint32_t event_index = 0; event_index < events.size(); ++event_index) { + XEvent &event = events[event_index]; WindowID window_id = MAIN_WINDOW_ID; @@ -2406,10 +2567,6 @@ void DisplayServerX11::process_events() { } } - if (XFilterEvent(&event, None)) { - continue; - } - if (XGetEventData(x11_display, &event.xcookie)) { if (event.xcookie.type == GenericEvent && event.xcookie.extension == xi.opcode) { XIDeviceEvent *event_data = (XIDeviceEvent *)event.xcookie.data; @@ -2572,32 +2729,74 @@ void DisplayServerX11::process_events() { XFreeEventData(x11_display, &event.xcookie); switch (event.type) { - case Expose: + case MapNotify: { + DEBUG_LOG_X11("[%u] MapNotify window=%lu (%u) \n", frame, event.xmap.window, window_id); + + const WindowData &wd = windows[window_id]; + + // Set focus when menu window is started. + // RevertToPointerRoot is used to make sure we don't lose all focus in case + // a subwindow and its parent are both destroyed. + if (wd.menu_type && !wd.no_focus) { + XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime); + } + } break; + + case Expose: { + DEBUG_LOG_X11("[%u] Expose window=%lu (%u), count='%u' \n", frame, event.xexpose.window, window_id, event.xexpose.count); + Main::force_redraw(); - break; + } break; + + case NoExpose: { + DEBUG_LOG_X11("[%u] NoExpose drawable=%lu (%u) \n", frame, event.xnoexpose.drawable, window_id); - case NoExpose: windows[window_id].minimized = true; - break; + } break; case VisibilityNotify: { + DEBUG_LOG_X11("[%u] VisibilityNotify window=%lu (%u), state=%u \n", frame, event.xvisibility.window, window_id, event.xvisibility.state); + XVisibilityEvent *visibility = (XVisibilityEvent *)&event; windows[window_id].minimized = (visibility->state == VisibilityFullyObscured); } break; + case LeaveNotify: { + DEBUG_LOG_X11("[%u] LeaveNotify window=%lu (%u), mode='%u' \n", frame, event.xcrossing.window, window_id, event.xcrossing.mode); + if (!mouse_mode_grab) { _send_window_event(windows[window_id], WINDOW_EVENT_MOUSE_EXIT); } } break; + case EnterNotify: { + DEBUG_LOG_X11("[%u] EnterNotify window=%lu (%u), mode='%u' \n", frame, event.xcrossing.window, window_id, event.xcrossing.mode); + if (!mouse_mode_grab) { _send_window_event(windows[window_id], WINDOW_EVENT_MOUSE_ENTER); } } break; - case FocusIn: - windows[window_id].focused = true; - _send_window_event(windows[window_id], WINDOW_EVENT_FOCUS_IN); + + case FocusIn: { + DEBUG_LOG_X11("[%u] FocusIn window=%lu (%u), mode='%u' \n", frame, event.xfocus.window, window_id, event.xfocus.mode); + + WindowData &wd = windows[window_id]; + + wd.focused = true; + + if (wd.xic) { + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + XSetICFocus(wd.xic); + } + + // Keep track of focus order for overlapping windows. + static unsigned int focus_order = 0; + wd.focus_order = ++focus_order; + + _send_window_event(wd, WINDOW_EVENT_FOCUS_IN); if (mouse_mode_grab) { // Show and update the cursor if confined and the window regained focus. @@ -2621,9 +2820,6 @@ void DisplayServerX11::process_events() { XIGrabDevice(x11_display, xi.touch_devices[i], x11_window, CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, False, &xi.touch_event_mask); }*/ #endif - if (windows[window_id].xic) { - XSetICFocus(windows[window_id].xic); - } if (!app_focused) { if (OS::get_singleton()->get_main_loop()) { @@ -2631,12 +2827,24 @@ void DisplayServerX11::process_events() { } app_focused = true; } - break; + } break; + + case FocusOut: { + DEBUG_LOG_X11("[%u] FocusOut window=%lu (%u), mode='%u' \n", frame, event.xfocus.window, window_id, event.xfocus.mode); + + WindowData &wd = windows[window_id]; + + wd.focused = false; + + if (wd.xic) { + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + XUnsetICFocus(wd.xic); + } - case FocusOut: - windows[window_id].focused = false; Input::get_singleton()->release_pressed_events(); - _send_window_event(windows[window_id], WINDOW_EVENT_FOCUS_OUT); + _send_window_event(wd, WINDOW_EVENT_FOCUS_OUT); if (mouse_mode_grab) { for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { @@ -2665,14 +2873,23 @@ void DisplayServerX11::process_events() { } xi.state.clear(); #endif - if (windows[window_id].xic) { - XSetICFocus(windows[window_id].xic); + } break; + + case ConfigureNotify: { + DEBUG_LOG_X11("[%u] ConfigureNotify window=%lu (%u), event=%lu, above=%lu, override_redirect=%u \n", frame, event.xconfigure.window, window_id, event.xconfigure.event, event.xconfigure.above, event.xconfigure.override_redirect); + + const WindowData &wd = windows[window_id]; + + // Set focus when menu window is re-used. + // RevertToPointerRoot is used to make sure we don't lose all focus in case + // a subwindow and its parent are both destroyed. + if (wd.menu_type && !wd.no_focus) { + XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime); } - break; - case ConfigureNotify: _window_changed(&event); - break; + } break; + case ButtonPress: case ButtonRelease: { /* exit in case of a mouse button press */ @@ -2699,7 +2916,18 @@ void DisplayServerX11::process_events() { mb->set_pressed((event.type == ButtonPress)); + const WindowData &wd = windows[window_id]; + if (event.type == ButtonPress) { + DEBUG_LOG_X11("[%u] ButtonPress window=%lu (%u), button_index=%u \n", frame, event.xbutton.window, window_id, mb->get_button_index()); + + // Ensure window focus on click. + // RevertToPointerRoot is used to make sure we don't lose all focus in case + // a subwindow and its parent are both destroyed. + if (!wd.no_focus) { + XSetInputFocus(x11_display, wd.x11_window, RevertToPointerRoot, CurrentTime); + } + uint64_t diff = OS::get_singleton()->get_ticks_usec() / 1000 - last_click_ms; if (mb->get_button_index() == last_click_button_index) { @@ -2718,6 +2946,33 @@ void DisplayServerX11::process_events() { last_click_ms += diff; last_click_pos = Point2i(event.xbutton.x, event.xbutton.y); } + } else { + DEBUG_LOG_X11("[%u] ButtonRelease window=%lu (%u), button_index=%u \n", frame, event.xbutton.window, window_id, mb->get_button_index()); + + if (!wd.focused) { + // Propagate the event to the focused window, + // because it's received only on the topmost window. + // Note: This is needed for drag & drop to work between windows, + // because the engine expects events to keep being processed + // on the same window dragging started. + for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + const WindowData &wd_other = E->get(); + WindowID window_id_other = E->key(); + if (wd_other.focused) { + if (window_id_other != window_id) { + int x, y; + Window child; + XTranslateCoordinates(x11_display, wd.x11_window, wd_other.x11_window, event.xbutton.x, event.xbutton.y, &x, &y, &child); + + mb->set_window_id(window_id_other); + mb->set_position(Vector2(x, y)); + mb->set_global_position(mb->get_position()); + Input::get_singleton()->accumulate_input_event(mb); + } + break; + } + } + } } Input::get_singleton()->accumulate_input_event(mb); @@ -2735,11 +2990,11 @@ void DisplayServerX11::process_events() { break; } - if (XPending(x11_display) > 0) { - XEvent tevent; - XPeekEvent(x11_display, &tevent); - if (tevent.type == MotionNotify) { - XNextEvent(x11_display, &event); + if (event_index + 1 < events.size()) { + const XEvent &next_event = events[event_index + 1]; + if (next_event.type == MotionNotify) { + ++event_index; + event = next_event; } else { break; } @@ -2767,6 +3022,9 @@ void DisplayServerX11::process_events() { break; } + const WindowData &wd = windows[window_id]; + bool focused = wd.focused; + if (mouse_mode == MOUSE_MODE_CAPTURED) { if (xi.relative_motion.x == 0 && xi.relative_motion.y == 0) { break; @@ -2775,7 +3033,7 @@ void DisplayServerX11::process_events() { Point2i new_center = pos; pos = last_mouse_pos + xi.relative_motion; center = new_center; - do_mouse_warp = windows[window_id].focused; // warp the cursor if we're focused in + do_mouse_warp = focused; // warp the cursor if we're focused in } if (!last_mouse_pos_valid) { @@ -2817,14 +3075,11 @@ void DisplayServerX11::process_events() { } mm->set_tilt(xi.tilt); - // Make the absolute position integral so it doesn't look _too_ weird :) - Point2i posi(pos); - _get_key_modifier_state(event.xmotion.state, mm); mm->set_button_mask(mouse_get_button_state()); - mm->set_position(posi); - mm->set_global_position(posi); - Input::get_singleton()->set_mouse_position(posi); + mm->set_position(pos); + mm->set_global_position(pos); + Input::get_singleton()->set_mouse_position(pos); mm->set_speed(Input::get_singleton()->get_last_mouse_speed()); mm->set_relative(rel); @@ -2835,8 +3090,32 @@ void DisplayServerX11::process_events() { // Don't propagate the motion event unless we have focus // this is so that the relative motion doesn't get messed up // after we regain focus. - if (windows[window_id].focused || !mouse_mode_grab) { + if (focused) { Input::get_singleton()->accumulate_input_event(mm); + } else { + // Propagate the event to the focused window, + // because it's received only on the topmost window. + // Note: This is needed for drag & drop to work between windows, + // because the engine expects events to keep being processed + // on the same window dragging started. + for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + const WindowData &wd_other = E->get(); + if (wd_other.focused) { + int x, y; + Window child; + XTranslateCoordinates(x11_display, wd.x11_window, wd_other.x11_window, event.xmotion.x, event.xmotion.y, &x, &y, &child); + + Point2i pos_focused(x, y); + + mm->set_window_id(E->key()); + mm->set_position(pos_focused); + mm->set_global_position(pos_focused); + mm->set_speed(Input::get_singleton()->get_last_mouse_speed()); + Input::get_singleton()->accumulate_input_event(mm); + + break; + } + } } } break; @@ -2846,67 +3125,7 @@ void DisplayServerX11::process_events() { // key event is a little complex, so // it will be handled in its own function. - _handle_key_event(window_id, (XKeyEvent *)&event); - } break; - case SelectionRequest: { - XSelectionRequestEvent *req; - XEvent e, respond; - e = event; - - req = &(e.xselectionrequest); - if (req->target == XInternAtom(x11_display, "UTF8_STRING", 0) || - req->target == XInternAtom(x11_display, "COMPOUND_TEXT", 0) || - req->target == XInternAtom(x11_display, "TEXT", 0) || - req->target == XA_STRING || - req->target == XInternAtom(x11_display, "text/plain;charset=utf-8", 0) || - req->target == XInternAtom(x11_display, "text/plain", 0)) { - CharString clip = clipboard_get().utf8(); - XChangeProperty(x11_display, - req->requestor, - req->property, - req->target, - 8, - PropModeReplace, - (unsigned char *)clip.get_data(), - clip.length()); - respond.xselection.property = req->property; - } else if (req->target == XInternAtom(x11_display, "TARGETS", 0)) { - Atom data[7]; - data[0] = XInternAtom(x11_display, "TARGETS", 0); - data[1] = XInternAtom(x11_display, "UTF8_STRING", 0); - data[2] = XInternAtom(x11_display, "COMPOUND_TEXT", 0); - data[3] = XInternAtom(x11_display, "TEXT", 0); - data[4] = XA_STRING; - data[5] = XInternAtom(x11_display, "text/plain;charset=utf-8", 0); - data[6] = XInternAtom(x11_display, "text/plain", 0); - - XChangeProperty(x11_display, - req->requestor, - req->property, - XA_ATOM, - 32, - PropModeReplace, - (unsigned char *)&data, - sizeof(data) / sizeof(data[0])); - respond.xselection.property = req->property; - - } else { - char *targetname = XGetAtomName(x11_display, req->target); - printf("No Target '%s'\n", targetname); - if (targetname) { - XFree(targetname); - } - respond.xselection.property = None; - } - - respond.xselection.type = SelectionNotify; - respond.xselection.display = req->display; - respond.xselection.requestor = req->requestor; - respond.xselection.selection = req->selection; - respond.xselection.target = req->target; - respond.xselection.time = req->time; - XSendEvent(x11_display, req->requestor, True, NoEventMask, &respond); - XFlush(x11_display); + _handle_key_event(window_id, (XKeyEvent *)&event, events, event_index); } break; case SelectionNotify: @@ -3213,12 +3432,37 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u unsigned long valuemask = CWBorderPixel | CWColormap | CWEventMask; - WindowID id; + WindowID id = window_id_counter++; + WindowData &wd = windows[id]; + + if ((id != MAIN_WINDOW_ID) && (p_flags & WINDOW_FLAG_BORDERLESS_BIT)) { + wd.menu_type = true; + } + + if (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) { + wd.menu_type = true; + wd.no_focus = true; + } + + // Setup for menu subwindows: + // - override_redirect forces the WM not to interfere with the window, to avoid delays due to + // handling decorations and placement. + // On the other hand, focus changes need to be handled manually when this is set. + // - save_under is a hint for the WM to keep the content of windows behind to avoid repaint. + if (wd.menu_type) { + windowAttributes.override_redirect = True; + windowAttributes.save_under = True; + valuemask |= CWOverrideRedirect | CWSaveUnder; + } + { - WindowData wd; wd.x11_window = XCreateWindow(x11_display, RootWindow(x11_display, visualInfo->screen), p_rect.position.x, p_rect.position.y, p_rect.size.width > 0 ? p_rect.size.width : 1, p_rect.size.height > 0 ? p_rect.size.height : 1, 0, visualInfo->depth, InputOutput, visualInfo->visual, valuemask, &windowAttributes); - XMapWindow(x11_display, wd.x11_window); + // Enable receiving notification when the window is initialized (MapNotify) + // so the focus can be set at the right time. + if (wd.menu_type && !wd.no_focus) { + XSelectInput(x11_display, wd.x11_window, StructureNotifyMask); + } //associate PID // make PID known to X11 @@ -3276,6 +3520,10 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u XChangeProperty(x11_display, wd.x11_window, xdnd_aware, XA_ATOM, 32, PropModeReplace, (unsigned char *)&xdnd_version, 1); if (xim && xim_style) { + // Block events polling while changing input focus + // because it triggers some event polling internally. + MutexLock mutex_lock(events_mutex); + wd.xic = XCreateIC(xim, XNInputStyle, xim_style, XNClientWindow, wd.x11_window, XNFocusWindow, wd.x11_window, (char *)nullptr); if (XGetICValues(wd.xic, XNFilterEvents, &im_event_mask, nullptr) != nullptr) { WARN_PRINT("XGetICValues couldn't obtain XNFilterEvents value"); @@ -3294,87 +3542,30 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u _update_context(wd); - id = window_id_counter++; - - windows[id] = wd; - - { - if (p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT) { - XSizeHints *xsh; - xsh = XAllocSizeHints(); - - xsh->flags = PMinSize | PMaxSize; - xsh->min_width = p_rect.size.width; - xsh->max_width = p_rect.size.width; - xsh->min_height = p_rect.size.height; - xsh->max_height = p_rect.size.height; - - XSetWMNormalHints(x11_display, wd.x11_window, xsh); - XFree(xsh); - } - - bool make_utility = false; - - if (p_flags & WINDOW_FLAG_BORDERLESS_BIT) { - Hints hints; - Atom property; - hints.flags = 2; - hints.decorations = 0; - property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True); - XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); - - make_utility = true; - } - if (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) { - make_utility = true; - } - - if (make_utility) { - //this one seems to disable the fade animations for regular windows - //but has the drawback that will not get focus by default, so - //we need to force it, unless no focus requested - - Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_UTILITY", False); - Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - - XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); - - if (!(p_flags & WINDOW_FLAG_NO_FOCUS_BIT)) { - //but as utility appears unfocused, it needs to be forcefuly focused, unless no focus requested - XEvent xev; - Atom net_active_window = XInternAtom(x11_display, "_NET_ACTIVE_WINDOW", False); - - memset(&xev, 0, sizeof(xev)); - xev.type = ClientMessage; - xev.xclient.window = wd.x11_window; - xev.xclient.message_type = net_active_window; - xev.xclient.format = 32; - xev.xclient.data.l[0] = 1; - xev.xclient.data.l[1] = CurrentTime; - - XSendEvent(x11_display, DefaultRootWindow(x11_display), False, SubstructureRedirectMask | SubstructureNotifyMask, &xev); - } - } else { - Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_NORMAL", False); - Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - - XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); - } + if (p_flags & WINDOW_FLAG_BORDERLESS_BIT) { + Hints hints; + Atom property; + hints.flags = 2; + hints.decorations = 0; + property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True); + XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5); } - if (id != MAIN_WINDOW_ID) { - XSizeHints my_hints = XSizeHints(); + if (wd.menu_type) { + // Set Utility type to disable fade animations. + Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_UTILITY", False); + Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - my_hints.flags = PPosition | PSize; /* I want to specify position and size */ - my_hints.x = p_rect.position.x; /* The origin and size coords I want */ - my_hints.y = p_rect.position.y; - my_hints.width = p_rect.size.width; - my_hints.height = p_rect.size.height; + XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); + } else { + Atom type_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE_NORMAL", False); + Atom wt_atom = XInternAtom(x11_display, "_NET_WM_WINDOW_TYPE", False); - XSetNormalHints(x11_display, wd.x11_window, &my_hints); - XMoveWindow(x11_display, wd.x11_window, p_rect.position.x, p_rect.position.y); + XChangeProperty(x11_display, wd.x11_window, wt_atom, XA_ATOM, 32, PropModeReplace, (unsigned char *)&type_atom, 1); } + _update_size_hints(id); + #if defined(VULKAN_ENABLED) if (context_vulkan) { Error err = context_vulkan->window_create(id, wd.x11_window, x11_display, p_rect.size.width, p_rect.size.height); @@ -3391,8 +3582,6 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u XFree(visualInfo); } - WindowData &wd = windows[id]; - window_set_mode(p_mode, id); //sync size @@ -3414,6 +3603,7 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, u if (cursors[current_cursor] != None) { XDefineCursor(x11_display, wd.x11_window, cursors[current_cursor]); } + return id; } @@ -3485,7 +3675,12 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode xrandr_handle = dlopen("libXrandr.so.2", RTLD_LAZY); if (!xrandr_handle) { err = dlerror(); - fprintf(stderr, "could not load libXrandr.so.2, Error: %s\n", err); + // For some arcane reason, NetBSD now ships libXrandr.so.3 while the rest of the world has libXrandr.so.2... + // In case this happens for other X11 platforms in the future, let's give it a try too before failing. + xrandr_handle = dlopen("libXrandr.so.3", RTLD_LAZY); + if (!xrandr_handle) { + fprintf(stderr, "could not load libXrandr.so.2, Error: %s\n", err); + } } else { XRRQueryVersion(x11_display, &xrandr_major, &xrandr_minor); if (((xrandr_major << 8) | xrandr_minor) >= 0x0105) { @@ -3653,6 +3848,7 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode window_set_flag(WindowFlags(i), true, main_window); } } + show_window(main_window); //create RenderingDevice if used #if defined(VULKAN_ENABLED) @@ -3830,12 +4026,19 @@ DisplayServerX11::DisplayServerX11(const String &p_rendering_driver, WindowMode } } + events_thread = Thread::create(_poll_events_thread, this); + _update_real_mouse_position(windows[MAIN_WINDOW_ID]); r_error = OK; } DisplayServerX11::~DisplayServerX11() { + events_thread_done = true; + Thread::wait_to_finish(events_thread); + memdelete(events_thread); + events_thread = nullptr; + //destroy all windows for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { #ifdef VULKAN_ENABLED @@ -3844,11 +4047,13 @@ DisplayServerX11::~DisplayServerX11() { } #endif - if (E->get().xic) { - XDestroyIC(E->get().xic); + WindowData &wd = E->get(); + if (wd.xic) { + XDestroyIC(wd.xic); + wd.xic = nullptr; } - XUnmapWindow(x11_display, E->get().x11_window); - XDestroyWindow(x11_display, E->get().x11_window); + XUnmapWindow(x11_display, wd.x11_window); + XDestroyWindow(x11_display, wd.x11_window); } //destroy drivers diff --git a/platform/linuxbsd/display_server_x11.h b/platform/linuxbsd/display_server_x11.h index b5d2ea1c63..740bf81fd9 100644 --- a/platform/linuxbsd/display_server_x11.h +++ b/platform/linuxbsd/display_server_x11.h @@ -36,6 +36,7 @@ #include "servers/display_server.h" #include "core/input/input.h" +#include "core/local_vector.h" #include "drivers/alsa/audio_driver_alsa.h" #include "drivers/alsamidi/midi_driver_alsamidi.h" #include "drivers/pulseaudio/audio_driver_pulseaudio.h" @@ -132,6 +133,9 @@ class DisplayServerX11 : public DisplayServer { ObjectID instance_id; + bool menu_type = false; + bool no_focus = false; + //better to guess on the fly, given WM can change it //WindowMode mode; bool fullscreen = false; //OS can't exit from this mode @@ -141,6 +145,8 @@ class DisplayServerX11 : public DisplayServer { Vector2i last_position_before_fs; bool focused = false; bool minimized = false; + + unsigned int focus_order = 0; }; Map<WindowID, WindowData> windows; @@ -197,7 +203,11 @@ class DisplayServerX11 : public DisplayServer { MouseMode mouse_mode; Point2i center; - void _handle_key_event(WindowID p_window, XKeyEvent *p_event, bool p_echo = false); + void _handle_key_event(WindowID p_window, XKeyEvent *p_event, LocalVector<XEvent> &p_events, uint32_t &p_event_index, bool p_echo = false); + void _handle_selection_request_event(XSelectionRequestEvent *p_event); + + String _clipboard_get_impl(Atom p_source, Window x11_window, Atom target) const; + String _clipboard_get(Atom p_source, Window x11_window) const; //bool minimized; //bool window_has_focus; @@ -235,6 +245,7 @@ class DisplayServerX11 : public DisplayServer { void _update_real_mouse_position(const WindowData &wd); bool _window_maximize_check(WindowID p_window, const char *p_atom_name) const; + void _update_size_hints(WindowID p_window); void _set_wm_fullscreen(WindowID p_window, bool p_enabled); void _set_wm_maximized(WindowID p_window, bool p_enabled); @@ -246,6 +257,16 @@ class DisplayServerX11 : public DisplayServer { static void _dispatch_input_events(const Ref<InputEvent> &p_event); void _dispatch_input_event(const Ref<InputEvent> &p_event); + mutable Mutex events_mutex; + Thread *events_thread = nullptr; + bool events_thread_done = false; + LocalVector<XEvent> polled_events; + static void _poll_events_thread(void *ud); + void _poll_events(); + + static Bool _predicate_all_events(Display *display, XEvent *event, XPointer arg); + static Bool _predicate_clipboard_selection(Display *display, XEvent *event, XPointer arg); + protected: void _window_changed(XEvent *event); @@ -276,6 +297,7 @@ public: virtual Vector<DisplayServer::WindowID> get_window_list() const; virtual WindowID create_sub_window(WindowMode p_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()); + virtual void show_window(WindowID p_id); virtual void delete_sub_window(WindowID p_id); virtual WindowID get_window_at_screen_position(const Point2i &p_position) const; @@ -284,6 +306,8 @@ public: virtual ObjectID window_get_attached_instance_id(WindowID p_window = MAIN_WINDOW_ID) const; virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID); + virtual void window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window = MAIN_WINDOW_ID); + virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); virtual void window_set_window_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); virtual void window_set_input_event_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); diff --git a/platform/linuxbsd/godot_linuxbsd.cpp b/platform/linuxbsd/godot_linuxbsd.cpp index 3ed64e9d46..e1796ccefe 100644 --- a/platform/linuxbsd/godot_linuxbsd.cpp +++ b/platform/linuxbsd/godot_linuxbsd.cpp @@ -41,6 +41,9 @@ int main(int argc, char *argv[]) { setlocale(LC_CTYPE, ""); + // We must override main when testing is enabled + TEST_MAIN_OVERRIDE + char *cwd = (char *)malloc(PATH_MAX); ERR_FAIL_COND_V(!cwd, ERR_OUT_OF_MEMORY); char *ret = getcwd(cwd, PATH_MAX); diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp index 5edaf35c50..fda1358dfd 100644 --- a/platform/linuxbsd/joypad_linux.cpp +++ b/platform/linuxbsd/joypad_linux.cpp @@ -311,16 +311,9 @@ void JoypadLinux::open_joypad(const char *p_path) { return; } - //check if the device supports basic gamepad events, prevents certain keyboards from - //being detected as joypads + // Check if the device supports basic gamepad events if (!(test_bit(EV_KEY, evbit) && test_bit(EV_ABS, evbit) && - (test_bit(ABS_X, absbit) || test_bit(ABS_Y, absbit) || test_bit(ABS_HAT0X, absbit) || - test_bit(ABS_GAS, absbit) || test_bit(ABS_RUDDER, absbit)) && - (test_bit(BTN_A, keybit) || test_bit(BTN_THUMBL, keybit) || - test_bit(BTN_TRIGGER, keybit) || test_bit(BTN_1, keybit))) && - !(test_bit(EV_ABS, evbit) && - test_bit(ABS_X, absbit) && test_bit(ABS_Y, absbit) && - test_bit(ABS_RX, absbit) && test_bit(ABS_RY, absbit))) { + test_bit(ABS_X, absbit) && test_bit(ABS_Y, absbit))) { close(fd); return; } diff --git a/platform/linuxbsd/os_linuxbsd.cpp b/platform/linuxbsd/os_linuxbsd.cpp index 8c6f3b1167..e00a32e3ba 100644 --- a/platform/linuxbsd/os_linuxbsd.cpp +++ b/platform/linuxbsd/os_linuxbsd.cpp @@ -88,7 +88,9 @@ void OS_LinuxBSD::finalize() { #endif #ifdef JOYDEV_ENABLED - memdelete(joypad); + if (joypad) { + memdelete(joypad); + } #endif } diff --git a/platform/linuxbsd/os_linuxbsd.h b/platform/linuxbsd/os_linuxbsd.h index 4295721c68..cd4fbd9db5 100644 --- a/platform/linuxbsd/os_linuxbsd.h +++ b/platform/linuxbsd/os_linuxbsd.h @@ -48,7 +48,7 @@ class OS_LinuxBSD : public OS_Unix { bool force_quit; #ifdef JOYDEV_ENABLED - JoypadLinux *joypad; + JoypadLinux *joypad = nullptr; #endif #ifdef ALSA_ENABLED diff --git a/platform/linuxbsd/platform_config.h b/platform/linuxbsd/platform_config.h index ac30519132..571ad03db0 100644 --- a/platform/linuxbsd/platform_config.h +++ b/platform/linuxbsd/platform_config.h @@ -31,9 +31,15 @@ #ifdef __linux__ #include <alloca.h> #endif -#if defined(__FreeBSD__) || defined(__OpenBSD__) -#include <stdlib.h> + +#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +#include <stdlib.h> // alloca +// FreeBSD and OpenBSD use pthread_set_name_np, while other platforms, +// include NetBSD, use pthread_setname_np. NetBSD's version however requires +// a different format, we handle this directly in thread_posix. +#ifdef __NetBSD__ +#define PTHREAD_NETBSD_SET_NAME +#else #define PTHREAD_BSD_SET_NAME #endif - -#define GLES2_INCLUDE_H "thirdparty/glad/glad/glad.h" +#endif diff --git a/platform/osx/crash_handler_osx.mm b/platform/osx/crash_handler_osx.mm index 5da0118686..9fb2f63935 100644 --- a/platform/osx/crash_handler_osx.mm +++ b/platform/osx/crash_handler_osx.mm @@ -90,7 +90,7 @@ static void handle_crash(int sig) { if (OS::get_singleton()->get_main_loop()) OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_CRASH); - fprintf(stderr, "Dumping the backtrace. %ls\n", msg.c_str()); + fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); char **strings = backtrace_symbols(bt_buffer, size); if (strings) { void *load_addr = (void *)load_address(); @@ -142,7 +142,7 @@ static void handle_crash(int sig) { } } - fprintf(stderr, "[%zu] %ls\n", i, output.c_str()); + fprintf(stderr, "[%zu] %s\n", i, output.utf8().get_data()); } free(strings); diff --git a/platform/osx/detect.py b/platform/osx/detect.py index 25d230fc89..50e9bd2653 100644 --- a/platform/osx/detect.py +++ b/platform/osx/detect.py @@ -1,6 +1,5 @@ import os import sys -import subprocess from methods import detect_darwin_sdk_path @@ -28,7 +27,8 @@ def get_opts(): ("MACOS_SDK_PATH", "Path to the macOS SDK", ""), BoolVariable( "use_static_mvk", - "Link MoltenVK statically as Level-0 driver (better portability) or use Vulkan ICD loader (enables validation layers)", + "Link MoltenVK statically as Level-0 driver (better portability) or use Vulkan ICD loader (enables" + " validation layers)", False, ), EnumVariable("debug_symbols", "Add debugging symbols to release builds", "yes", ("yes", "no", "full")), @@ -50,9 +50,11 @@ def configure(env): if env["target"] == "release": if env["optimize"] == "speed": # optimize for speed (default) - env.Prepend(CCFLAGS=["-O3", "-fomit-frame-pointer", "-ftree-vectorize", "-msse2"]) + env.Prepend(CCFLAGS=["-O3", "-fomit-frame-pointer", "-ftree-vectorize"]) else: # optimize for size - env.Prepend(CCFLAGS=["-Os", "-ftree-vectorize", "-msse2"]) + env.Prepend(CCFLAGS=["-Os", "-ftree-vectorize"]) + if env["arch"] != "arm64": + env.Prepend(CCFLAGS=["-msse2"]) if env["debug_symbols"] == "yes": env.Prepend(CCFLAGS=["-g1"]) @@ -73,6 +75,7 @@ def configure(env): elif env["target"] == "debug": env.Prepend(CCFLAGS=["-g3"]) env.Prepend(CPPDEFINES=["DEBUG_ENABLED"]) + env.Prepend(LINKFLAGS=["-Xlinker", "-no_deduplicate"]) ## Architecture @@ -136,7 +139,8 @@ def configure(env): env.Append(CPPDEFINES=["__MACPORTS__"]) # hack to fix libvpx MM256_BROADCASTSI128_SI256 define if env["CXX"] == "clang++": - env.Append(CPPDEFINES=["TYPED_METHOD_BIND"]) + # This should now work with clang++, re-enable if there are issues + # env.Append(CPPDEFINES=["TYPED_METHOD_BIND"]) env["CC"] = "clang" env["LINK"] = "clang++" diff --git a/platform/osx/dir_access_osx.h b/platform/osx/dir_access_osx.h index d61ee181f0..91b8f9b2c5 100644 --- a/platform/osx/dir_access_osx.h +++ b/platform/osx/dir_access_osx.h @@ -47,6 +47,8 @@ protected: virtual int get_drive_count(); virtual String get_drive(int p_drive); + + virtual bool is_hidden(const String &p_name); }; #endif //UNIX ENABLED diff --git a/platform/osx/dir_access_osx.mm b/platform/osx/dir_access_osx.mm index 7791ba5407..439c6a075f 100644 --- a/platform/osx/dir_access_osx.mm +++ b/platform/osx/dir_access_osx.mm @@ -68,4 +68,14 @@ String DirAccessOSX::get_drive(int p_drive) { return volname; } +bool DirAccessOSX::is_hidden(const String &p_name) { + String f = get_current_dir().plus_file(p_name); + NSURL *url = [NSURL fileURLWithPath:@(f.utf8().get_data())]; + NSNumber *hidden = nil; + if (![url getResourceValue:&hidden forKey:NSURLIsHiddenKey error:nil]) { + return DirAccessUnix::is_hidden(p_name); + } + return [hidden boolValue]; +} + #endif //posix_enabled diff --git a/platform/osx/display_server_osx.h b/platform/osx/display_server_osx.h index 68e8454fd0..073d35008b 100644 --- a/platform/osx/display_server_osx.h +++ b/platform/osx/display_server_osx.h @@ -102,6 +102,8 @@ public: id window_object; id window_view; + Vector<Vector2> mpath; + #if defined(OPENGL_ENABLED) ContextGL_OSX *context_gles2 = nullptr; #endif @@ -230,6 +232,7 @@ public: virtual Vector<int> get_window_list() const override; virtual WindowID create_sub_window(WindowMode p_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()) override; + virtual void show_window(WindowID p_id) override; virtual void delete_sub_window(WindowID p_id) override; virtual void window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; @@ -239,6 +242,7 @@ public: virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID) override; virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID) override; + virtual void window_set_mouse_passthrough(const Vector<Vector2> &p_region, 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; diff --git a/platform/osx/display_server_osx.mm b/platform/osx/display_server_osx.mm index 5e209a6e6b..1ad7117b39 100644 --- a/platform/osx/display_server_osx.mm +++ b/platform/osx/display_server_osx.mm @@ -33,6 +33,7 @@ #include "os_osx.h" #include "core/io/marshalls.h" +#include "core/math/geometry_2d.h" #include "core/os/keyboard.h" #include "main/main.h" #include "scene/resources/texture.h" @@ -45,7 +46,6 @@ #include <IOKit/hid/IOHIDLib.h> #if defined(OPENGL_ENABLED) -#include "drivers/gles2/rasterizer_gles2.h" //TODO - reimplement OpenGLES #import <AppKit/NSOpenGLView.h> @@ -63,6 +63,8 @@ #define DS_OSX ((DisplayServerOSX *)(DisplayServerOSX::get_singleton())) +static bool ignore_momentum_scroll = false; + static void _get_key_modifier_state(unsigned int p_osx_state, Ref<InputEventWithModifiers> r_state) { r_state->set_shift((p_osx_state & NSEventModifierFlagShift)); r_state->set_control((p_osx_state & NSEventModifierFlagControl)); @@ -312,8 +314,6 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { DS_OSX->window_set_transient(wd.transient_children.front()->get(), DisplayServerOSX::INVALID_WINDOW_ID); } - DS_OSX->windows.erase(window_id); - if (wd.transient_parent != DisplayServerOSX::INVALID_WINDOW_ID) { DisplayServerOSX::WindowData &pwd = DS_OSX->windows[wd.transient_parent]; [pwd.window_object makeKeyAndOrderFront:nil]; // Move focus back to parent. @@ -333,6 +333,8 @@ static NSCursor *_cursorFromSelector(SEL selector, SEL fallback = nil) { DS_OSX->context_vulkan->window_destroy(window_id); } #endif + + DS_OSX->windows.erase(window_id); } - (void)windowDidEnterFullScreen:(NSNotification *)notification { @@ -1304,6 +1306,8 @@ static int remapKey(unsigned int key, unsigned int state) { ERR_FAIL_COND(!DS_OSX->windows.has(window_id)); DisplayServerOSX::WindowData &wd = DS_OSX->windows[window_id]; + ignore_momentum_scroll = true; + // Ignore all input if IME input is in progress if (!imeInputEventInProgress) { NSString *characters = [event characters]; @@ -1348,6 +1352,8 @@ static int remapKey(unsigned int key, unsigned int state) { } - (void)flagsChanged:(NSEvent *)event { + ignore_momentum_scroll = true; + // Ignore all input if IME input is in progress if (!imeInputEventInProgress) { DisplayServerOSX::KeyEvent ke; @@ -1507,6 +1513,14 @@ inline void sendPanEvent(DisplayServer::WindowID window_id, double dx, double dy deltaY *= 0.03; } + if ([event momentumPhase] != NSEventPhaseNone) { + if (ignore_momentum_scroll) { + return; + } + } else { + ignore_momentum_scroll = false; + } + if ([event phase] != NSEventPhaseNone || [event momentumPhase] != NSEventPhaseNone) { sendPanEvent(window_id, deltaX, deltaY, [event modifierFlags]); } else { @@ -1944,8 +1958,12 @@ void DisplayServerOSX::alert(const String &p_alert, const String &p_title) { [window setInformativeText:ns_alert]; [window setAlertStyle:NSAlertStyleWarning]; + id key_window = [[NSApplication sharedApplication] keyWindow]; [window runModal]; [window release]; + if (key_window) { + [key_window makeKeyAndOrderFront:nil]; + } } Error DisplayServerOSX::dialog_show(String p_title, String p_description, Vector<String> p_buttons, const Callable &p_callback) { @@ -2037,6 +2055,12 @@ void DisplayServerOSX::mouse_set_mode(MouseMode p_mode) { CGDisplayHideCursor(kCGDirectMainDisplay); } CGAssociateMouseAndMouseCursorPosition(false); + WindowData &wd = windows[MAIN_WINDOW_ID]; + const NSRect contentRect = [wd.window_view frame]; + NSRect pointInWindowRect = NSMakeRect(contentRect.size.width / 2, contentRect.size.height / 2, 0, 0); + NSPoint pointOnScreen = [[wd.window_view window] convertRectToScreen:pointInWindowRect].origin; + CGPoint lMouseWarpPos = { pointOnScreen.x, CGDisplayBounds(CGMainDisplayID()).size.height - pointOnScreen.y }; + CGWarpMouseCursorPosition(lMouseWarpPos); } else if (p_mode == MOUSE_MODE_HIDDEN) { if (mouse_mode == MOUSE_MODE_VISIBLE || mouse_mode == MOUSE_MODE_CONFINED) { CGDisplayHideCursor(kCGDirectMainDisplay); @@ -2236,11 +2260,18 @@ int DisplayServerOSX::screen_get_dpi(int p_screen) const { NSArray *screenArray = [NSScreen screens]; if ((NSUInteger)p_screen < [screenArray count]) { NSDictionary *description = [[screenArray objectAtIndex:p_screen] deviceDescription]; - NSSize displayDPI = [[description objectForKey:NSDeviceResolution] sizeValue]; - return (displayDPI.width + displayDPI.height) / 2; + + const NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; + const CGSize displayPhysicalSize = CGDisplayScreenSize([[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); + float scale = [[screenArray objectAtIndex:p_screen] backingScaleFactor]; + + float den2 = (displayPhysicalSize.width / 25.4f) * (displayPhysicalSize.width / 25.4f) + (displayPhysicalSize.height / 25.4f) * (displayPhysicalSize.height / 25.4f); + if (den2 > 0.0f) { + return ceil(sqrt(displayPixelSize.width * displayPixelSize.width + displayPixelSize.height * displayPixelSize.height) / sqrt(den2) * scale); + } } - return 96; + return 72; } float DisplayServerOSX::screen_get_scale(int p_screen) const { @@ -2311,18 +2342,23 @@ DisplayServer::WindowID DisplayServerOSX::create_sub_window(WindowMode p_mode, u _THREAD_SAFE_METHOD_ WindowID id = _create_window(p_mode, p_rect); - WindowData &wd = windows[id]; for (int i = 0; i < WINDOW_FLAG_MAX; i++) { if (p_flags & (1 << i)) { window_set_flag(WindowFlags(i), true, id); } } + + return id; +} + +void DisplayServerOSX::show_window(WindowID p_id) { + WindowData &wd = windows[p_id]; + if (wd.no_focus) { [wd.window_object orderFront:nil]; } else { [wd.window_object makeKeyAndOrderFront:nil]; } - return id; } void DisplayServerOSX::_send_window_event(const WindowData &wd, WindowEvent p_event) { @@ -2366,7 +2402,11 @@ void DisplayServerOSX::_update_window(WindowData p_wd) { [p_wd.window_object setHidesOnDeactivate:YES]; } else { // Reset these when our window is not a borderless window that covers up the screen - [p_wd.window_object setLevel:NSNormalWindowLevel]; + if (p_wd.on_top) { + [p_wd.window_object setLevel:NSFloatingWindowLevel]; + } else { + [p_wd.window_object setLevel:NSNormalWindowLevel]; + } [p_wd.window_object setHidesOnDeactivate:NO]; } } @@ -2392,6 +2432,15 @@ void DisplayServerOSX::window_set_title(const String &p_title, WindowID p_window [wd.window_object setTitle:[NSString stringWithUTF8String:p_title.utf8().get_data()]]; } +void DisplayServerOSX::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!windows.has(p_window)); + WindowData &wd = windows[p_window]; + + wd.mpath = p_region; +} + void DisplayServerOSX::window_set_rect_changed_callback(const Callable &p_callable, WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -2793,7 +2842,9 @@ void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo } break; case WINDOW_FLAG_BORDERLESS: { // OrderOut prevents a lose focus bug with the window - [wd.window_object orderOut:nil]; + if ([wd.window_object isVisible]) { + [wd.window_object orderOut:nil]; + } wd.borderless = p_enabled; if (p_enabled) { [wd.window_object setStyleMask:NSWindowStyleMaskBorderless]; @@ -2807,7 +2858,13 @@ void DisplayServerOSX::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo [wd.window_object setFrame:frameRect display:NO]; } _update_window(wd); - [wd.window_object makeKeyAndOrderFront:nil]; + if ([wd.window_object isVisible]) { + if (wd.no_focus) { + [wd.window_object orderFront:nil]; + } else { + [wd.window_object makeKeyAndOrderFront:nil]; + } + } } break; case WINDOW_FLAG_ALWAYS_ON_TOP: { wd.on_top = p_enabled; @@ -2875,7 +2932,11 @@ void DisplayServerOSX::window_move_to_foreground(WindowID p_window) { const WindowData &wd = windows[p_window]; [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; - [wd.window_object makeKeyAndOrderFront:nil]; + if (wd.no_focus) { + [wd.window_object orderFront:nil]; + } else { + [wd.window_object makeKeyAndOrderFront:nil]; + } } bool DisplayServerOSX::window_can_draw(WindowID p_window) const { @@ -3332,6 +3393,26 @@ void DisplayServerOSX::process_events() { Input::get_singleton()->flush_accumulated_events(); } + for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { + WindowData &wd = E->get(); + if (wd.mpath.size() > 0) { + const Vector2 mpos = _get_mouse_pos(wd, [wd.window_object mouseLocationOutsideOfEventStream]); + if (Geometry2D::is_point_in_polygon(mpos, wd.mpath)) { + if ([wd.window_object ignoresMouseEvents]) { + [wd.window_object setIgnoresMouseEvents:NO]; + } + } else { + if (![wd.window_object ignoresMouseEvents]) { + [wd.window_object setIgnoresMouseEvents:YES]; + } + } + } else { + if ([wd.window_object ignoresMouseEvents]) { + [wd.window_object setIgnoresMouseEvents:NO]; + } + } + } + [autoreleasePool drain]; autoreleasePool = [[NSAutoreleasePool alloc] init]; } @@ -3755,7 +3836,7 @@ DisplayServerOSX::DisplayServerOSX(const String &p_rendering_driver, WindowMode window_set_flag(WindowFlags(i), true, main_window); } } - [windows[main_window].window_object makeKeyAndOrderFront:nil]; + show_window(MAIN_WINDOW_ID); #if defined(OPENGL_ENABLED) if (rendering_driver == "opengl_es") { @@ -3784,9 +3865,11 @@ DisplayServerOSX::~DisplayServerOSX() { } //destroy all windows - for (Map<WindowID, WindowData>::Element *E = windows.front(); E; E = E->next()) { - [E->get().window_object setContentView:nil]; - [E->get().window_object close]; + for (Map<WindowID, WindowData>::Element *E = windows.front(); E;) { + Map<WindowID, WindowData>::Element *F = E; + E = E->next(); + [F->get().window_object setContentView:nil]; + [F->get().window_object close]; } //destroy drivers diff --git a/platform/osx/export/export.cpp b/platform/osx/export/export.cpp index ae45e0734d..9f2160dd9e 100644 --- a/platform/osx/export/export.cpp +++ b/platform/osx/export/export.cpp @@ -78,7 +78,7 @@ class EditorExportPlatformOSX : public EditorExportPlatform { } for (int i = 0; i < pname.length(); i++) { - CharType c = pname[i]; + char32_t c = pname[i]; if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.')) { if (r_error) { *r_error = vformat(TTR("The character '%s' is not allowed in Identifier."), String::chr(c)); @@ -95,8 +95,8 @@ protected: virtual void get_export_options(List<ExportOption> *r_options) override; public: - virtual String get_name() const override { return "Mac OSX"; } - virtual String get_os_name() const override { return "OSX"; } + virtual String get_name() const override { return "macOS"; } + virtual String get_os_name() const override { return "macOS"; } virtual Ref<Texture2D> get_logo() const override { return logo; } virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override { diff --git a/platform/osx/godot_main_osx.mm b/platform/osx/godot_main_osx.mm index 93d0d6168c..4e73d5441c 100644 --- a/platform/osx/godot_main_osx.mm +++ b/platform/osx/godot_main_osx.mm @@ -37,7 +37,7 @@ int main(int argc, char **argv) { #if defined(VULKAN_ENABLED) - //MoltenVK - enable full component swizzling support + // MoltenVK - enable full component swizzling support setenv("MVK_CONFIG_FULL_IMAGE_VIEW_SWIZZLE", "1", 1); #endif @@ -60,6 +60,9 @@ int main(int argc, char **argv) { OS_OSX os; Error err; + // We must override main when testing is enabled + TEST_MAIN_OVERRIDE + if (os.open_with_filename != "") { char *argv_c = (char *)malloc(os.open_with_filename.utf8().size()); memcpy(argv_c, os.open_with_filename.utf8().get_data(), os.open_with_filename.utf8().size()); diff --git a/platform/osx/logo.png b/platform/osx/logo.png Binary files differindex 834bbf3ba6..b5a660b165 100644 --- a/platform/osx/logo.png +++ b/platform/osx/logo.png diff --git a/platform/osx/os_osx.h b/platform/osx/os_osx.h index 9204a145bf..5a9e43450f 100644 --- a/platform/osx/os_osx.h +++ b/platform/osx/os_osx.h @@ -44,7 +44,7 @@ class OS_OSX : public OS_Unix { bool force_quit; - JoypadOSX *joypad_osx; + JoypadOSX *joypad_osx = nullptr; #ifdef COREAUDIO_ENABLED AudioDriverCoreAudio audio_driver; diff --git a/platform/osx/os_osx.mm b/platform/osx/os_osx.mm index c4eb5407af..399a29cbe0 100644 --- a/platform/osx/os_osx.mm +++ b/platform/osx/os_osx.mm @@ -145,7 +145,9 @@ void OS_OSX::finalize() { delete_main_loop(); - memdelete(joypad_osx); + if (joypad_osx) { + memdelete(joypad_osx); + } } void OS_OSX::set_main_loop(MainLoop *p_main_loop) { diff --git a/platform/osx/platform_config.h b/platform/osx/platform_config.h index 155f37ed55..e657aca955 100644 --- a/platform/osx/platform_config.h +++ b/platform/osx/platform_config.h @@ -30,5 +30,4 @@ #include <alloca.h> -#define GLES2_INCLUDE_H "thirdparty/glad/glad/glad.h" #define PTHREAD_RENAME_SELF diff --git a/platform/server/godot_server.cpp b/platform/server/godot_server.cpp index 32bd943ac3..9f22240a80 100644 --- a/platform/server/godot_server.cpp +++ b/platform/server/godot_server.cpp @@ -34,6 +34,9 @@ int main(int argc, char *argv[]) { OS_Server os; + // We must override main when testing is enabled + TEST_MAIN_OVERRIDE + Error err = Main::setup(argv[0], argc - 1, &argv[1]); if (err != OK) return 255; diff --git a/platform/server/platform_config.h b/platform/server/platform_config.h index bdff93f02b..73136ec81b 100644 --- a/platform/server/platform_config.h +++ b/platform/server/platform_config.h @@ -31,10 +31,19 @@ #if defined(__linux__) || defined(__APPLE__) #include <alloca.h> #endif -#if defined(__FreeBSD__) || defined(__OpenBSD__) -#include <stdlib.h> + +#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +#include <stdlib.h> // alloca +// FreeBSD and OpenBSD use pthread_set_name_np, while other platforms, +// include NetBSD, use pthread_setname_np. NetBSD's version however requires +// a different format, we handle this directly in thread_posix. +#ifdef __NetBSD__ +#define PTHREAD_NETBSD_SET_NAME +#else #define PTHREAD_BSD_SET_NAME #endif +#endif + #ifdef __APPLE__ #define PTHREAD_RENAME_SELF #endif diff --git a/platform/uwp/detect.py b/platform/uwp/detect.py index c23a65ef75..2af7803749 100644 --- a/platform/uwp/detect.py +++ b/platform/uwp/detect.py @@ -65,12 +65,14 @@ def configure(env): env.Append(CCFLAGS=["/MD"]) env.Append(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) elif env["target"] == "debug": env.Append(CCFLAGS=["/Zi"]) env.Append(CCFLAGS=["/MDd"]) env.Append(CPPDEFINES=["DEBUG_ENABLED"]) env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) env.Append(LINKFLAGS=["/DEBUG"]) ## Compiler configuration @@ -78,6 +80,9 @@ def configure(env): env["ENV"] = os.environ vc_base_path = os.environ["VCTOOLSINSTALLDIR"] if "VCTOOLSINSTALLDIR" in os.environ else os.environ["VCINSTALLDIR"] + # Force to use Unicode encoding + env.AppendUnique(CCFLAGS=["/utf-8"]) + # ANGLE angle_root = os.getenv("ANGLE_SRC_PATH") env.Prepend(CPPPATH=[angle_root + "/include"]) @@ -120,7 +125,9 @@ def configure(env): print("Compiled program architecture will be a x86 executable. (forcing bits=32).") else: print( - "Failed to detect MSVC compiler architecture version... Defaulting to 32-bit executable settings (forcing bits=32). Compilation attempt will continue, but SCons can not detect for what architecture this build is compiled for. You should check your settings/compilation setup." + "Failed to detect MSVC compiler architecture version... Defaulting to 32-bit executable settings" + " (forcing bits=32). Compilation attempt will continue, but SCons can not detect for what architecture" + " this build is compiled for. You should check your settings/compilation setup." ) env["bits"] = "32" @@ -160,7 +167,10 @@ def configure(env): env.Append(CPPFLAGS=["/AI", vc_base_path + "lib/x86/store/references"]) env.Append( - CCFLAGS='/FS /MP /GS /wd"4453" /wd"28204" /wd"4291" /Zc:wchar_t /Gm- /fp:precise /errorReport:prompt /WX- /Zc:forScope /Gd /EHsc /nologo'.split() + CCFLAGS=( + '/FS /MP /GS /wd"4453" /wd"28204" /wd"4291" /Zc:wchar_t /Gm- /fp:precise /errorReport:prompt /WX-' + " /Zc:forScope /Gd /EHsc /nologo".split() + ) ) env.Append(CPPDEFINES=["_UNICODE", "UNICODE", ("WINAPI_FAMILY", "WINAPI_FAMILY_APP")]) env.Append(CXXFLAGS=["/ZW"]) diff --git a/platform/uwp/export/export.cpp b/platform/uwp/export/export.cpp index ede0d7c76b..e9e536837f 100644 --- a/platform/uwp/export/export.cpp +++ b/platform/uwp/export/export.cpp @@ -29,11 +29,12 @@ /*************************************************************************/ #include "export.h" + #include "core/bind/core_bind.h" +#include "core/class_db.h" #include "core/crypto/crypto_core.h" #include "core/io/marshalls.h" #include "core/io/zip_io.h" -#include "core/object.h" #include "core/os/dir_access.h" #include "core/os/file_access.h" #include "core/project_settings.h" @@ -249,7 +250,7 @@ void AppxPackager::make_content_types(const String &p_path) { Map<String, String> types; for (int i = 0; i < file_metadata.size(); i++) { - String ext = file_metadata[i].name.get_extension(); + String ext = file_metadata[i].name.get_extension().to_lower(); if (types.has(ext)) { continue; @@ -961,7 +962,7 @@ class EditorExportPlatformUWP : public EditorExportPlatform { return true; } - static Error save_appx_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total) { + static Error save_appx_file(void *p_userdata, const String &p_path, const Vector<uint8_t> &p_data, int p_file, int p_total, const Vector<String> &p_enc_in_filters, const Vector<String> &p_enc_ex_filters, const Vector<uint8_t> &p_key) { AppxPackager *packager = (AppxPackager *)p_userdata; String dst_path = p_path.replace_first("res://", "game/"); diff --git a/platform/uwp/os_uwp.cpp b/platform/uwp/os_uwp.cpp index ee25754704..44ab075816 100644 --- a/platform/uwp/os_uwp.cpp +++ b/platform/uwp/os_uwp.cpp @@ -35,7 +35,6 @@ #include "core/io/marshalls.h" #include "core/project_settings.h" -#include "drivers/gles2/rasterizer_gles2.h" #include "drivers/unix/ip_unix.h" #include "drivers/windows/dir_access_windows.h" #include "drivers/windows/file_access_windows.h" @@ -297,7 +296,7 @@ Error OS_UWP::initialize(const VideoMode &p_desired, int p_video_driver, int p_a void OS_UWP::set_clipboard(const String &p_text) { DataPackage ^ clip = ref new DataPackage(); clip->RequestedOperation = DataPackageOperation::Copy; - clip->SetText(ref new Platform::String((const wchar_t *)p_text.c_str())); + clip->SetText(ref new Platform::String((LPCWSTR)(p_text.utf16().get_data()))); Clipboard::SetContent(clip); }; @@ -347,8 +346,8 @@ void OS_UWP::finalize_core() { } void OS_UWP::alert(const String &p_alert, const String &p_title) { - Platform::String ^ alert = ref new Platform::String(p_alert.c_str()); - Platform::String ^ title = ref new Platform::String(p_title.c_str()); + Platform::String ^ alert = ref new Platform::String((LPCWSTR)(p_alert.utf16().get_data())); + Platform::String ^ title = ref new Platform::String((LPCWSTR)(p_title.utf16().get_data())); MessageDialog ^ msg = ref new MessageDialog(alert, title); @@ -715,7 +714,7 @@ bool OS_UWP::has_virtual_keyboard() const { return UIViewSettings::GetForCurrentView()->UserInteractionMode == UserInteractionMode::Touch; } -void OS_UWP::show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect, int p_max_input_length, int p_cursor_start, int p_cursor_end) { +void OS_UWP::show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect, bool p_multiline, int p_max_input_length, int p_cursor_start, int p_cursor_end) { InputPane ^ pane = InputPane::GetForCurrentView(); pane->TryShow(); } @@ -739,7 +738,7 @@ static String format_error_message(DWORD id) { Error OS_UWP::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path) { String full_path = "game/" + p_path; - p_library_handle = (void *)LoadPackagedLibrary(full_path.c_str(), 0); + p_library_handle = (void *)LoadPackagedLibrary((LPCWSTR)(full_path.utf16().get_data()), 0); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + full_path + ", error: " + format_error_message(GetLastError()) + "."); return OK; } diff --git a/platform/uwp/os_uwp.h b/platform/uwp/os_uwp.h index c35b634353..892327bac5 100644 --- a/platform/uwp/os_uwp.h +++ b/platform/uwp/os_uwp.h @@ -234,7 +234,7 @@ public: virtual bool has_touchscreen_ui_hint() const; virtual bool has_virtual_keyboard() const; - virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); + virtual void show_virtual_keyboard(const String &p_existing_text, const Rect2 &p_screen_rect = Rect2(), bool p_multiline = false, int p_max_input_length = -1, int p_cursor_start = -1, int p_cursor_end = -1); virtual void hide_virtual_keyboard(); virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false); diff --git a/platform/windows/SCsub b/platform/windows/SCsub index daffe59f34..e3f86977a4 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -26,10 +26,10 @@ prog = env.add_program("#bin/godot", common_win + res_obj, PROGSUFFIX=env["PROGS # Microsoft Visual Studio Project Generation if env["vsproj"]: - env.vs_srcs = env.vs_srcs + ["platform/windows/" + res_file] - env.vs_srcs = env.vs_srcs + ["platform/windows/godot.natvis"] + env.vs_srcs += ["platform/windows/" + res_file] + env.vs_srcs += ["platform/windows/godot.natvis"] for x in common_win: - env.vs_srcs = env.vs_srcs + ["platform/windows/" + str(x)] + env.vs_srcs += ["platform/windows/" + str(x)] if not os.getenv("VCINSTALLDIR"): if (env["debug_symbols"] == "full" or env["debug_symbols"] == "yes") and env["separate_debug_symbols"]: diff --git a/platform/windows/crash_handler_windows.cpp b/platform/windows/crash_handler_windows.cpp index 996d9722f5..02031ef6bb 100644 --- a/platform/windows/crash_handler_windows.cpp +++ b/platform/windows/crash_handler_windows.cpp @@ -175,7 +175,7 @@ DWORD CrashHandlerException(EXCEPTION_POINTERS *ep) { msg = proj_settings->get("debug/settings/crash_handler/message"); } - fprintf(stderr, "Dumping the backtrace. %ls\n", msg.c_str()); + fprintf(stderr, "Dumping the backtrace. %s\n", msg.utf8().get_data()); int n = 0; do { diff --git a/platform/windows/detect.py b/platform/windows/detect.py index 0ab0e1ed3c..489e45404f 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -65,6 +65,7 @@ def get_opts(): # Vista support dropped after EOL due to GH-10243 ("target_win_version", "Targeted Windows version, >= 0x0601 (Windows 7)", "0x0601"), EnumVariable("debug_symbols", "Add debugging symbols to release builds", "yes", ("yes", "no", "full")), + EnumVariable("windows_subsystem", "Windows subsystem", "default", ("default", "console", "gui")), BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False), ("msvc_version", "MSVC version to use. Ignored if VCINSTALLDIR is set in shell env.", None), BoolVariable("use_mingw", "Use the Mingw compiler, even if MSVC is installed. Only used on Windows.", False), @@ -128,7 +129,9 @@ def setup_msvc_manual(env): print("Compiled program architecture will be a 32 bit executable. (forcing bits=32).") else: print( - "Failed to manually detect MSVC compiler architecture version... Defaulting to 32bit executable settings (forcing bits=32). Compilation attempt will continue, but SCons can not detect for what architecture this build is compiled for. You should check your settings/compilation setup, or avoid setting VCINSTALLDIR." + "Failed to manually detect MSVC compiler architecture version... Defaulting to 32bit executable settings" + " (forcing bits=32). Compilation attempt will continue, but SCons can not detect for what architecture this" + " build is compiled for. You should check your settings/compilation setup, or avoid setting VCINSTALLDIR." ) @@ -176,12 +179,20 @@ def configure_msvc(env, manual_msvc_config): # Build type + if env["tests"]: + env["windows_subsystem"] = "console" + elif env["windows_subsystem"] == "default": + # Default means we use console for debug, gui for release. + if "debug" in env["target"]: + env["windows_subsystem"] = "console" + else: + env["windows_subsystem"] = "gui" + if env["target"] == "release": if env["optimize"] == "speed": # optimize for speed (default) env.Append(CCFLAGS=["/O2"]) else: # optimize for size env.Append(CCFLAGS=["/O1"]) - env.Append(LINKFLAGS=["/SUBSYSTEM:WINDOWS"]) env.Append(LINKFLAGS=["/ENTRY:mainCRTStartup"]) env.Append(LINKFLAGS=["/OPT:REF"]) @@ -191,24 +202,28 @@ def configure_msvc(env, manual_msvc_config): else: # optimize for size env.Append(CCFLAGS=["/O1"]) env.AppendUnique(CPPDEFINES=["DEBUG_ENABLED"]) - env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) env.Append(LINKFLAGS=["/OPT:REF"]) elif env["target"] == "debug": env.AppendUnique(CCFLAGS=["/Z7", "/Od", "/EHsc"]) env.AppendUnique(CPPDEFINES=["DEBUG_ENABLED"]) - env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) env.Append(LINKFLAGS=["/DEBUG"]) if env["debug_symbols"] == "full" or env["debug_symbols"] == "yes": env.AppendUnique(CCFLAGS=["/Z7"]) env.AppendUnique(LINKFLAGS=["/DEBUG"]) + if env["windows_subsystem"] == "gui": + env.Append(LINKFLAGS=["/SUBSYSTEM:WINDOWS"]) + else: + env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) + ## Compile/link flags env.AppendUnique(CCFLAGS=["/MT", "/Gd", "/GR", "/nologo"]) - if int(env["MSVC_VERSION"].split(".")[0]) >= 14: # vs2015 and later - env.AppendUnique(CCFLAGS=["/utf-8"]) + # Force to use Unicode encoding + env.AppendUnique(CCFLAGS=["/utf-8"]) env.AppendUnique(CXXFLAGS=["/TP"]) # assume all sources are C++ if manual_msvc_config: # should be automatic if SCons found it if os.getenv("WindowsSdkDir") is not None: @@ -301,6 +316,15 @@ def configure_mingw(env): ## Build type + if env["tests"]: + env["windows_subsystem"] = "console" + elif env["windows_subsystem"] == "default": + # Default means we use console for debug, gui for release. + if "debug" in env["target"]: + env["windows_subsystem"] = "console" + else: + env["windows_subsystem"] = "gui" + if env["target"] == "release": env.Append(CCFLAGS=["-msse2"]) @@ -312,8 +336,6 @@ def configure_mingw(env): else: # optimize for size env.Prepend(CCFLAGS=["-Os"]) - env.Append(LINKFLAGS=["-Wl,--subsystem,windows"]) - if env["debug_symbols"] == "yes": env.Prepend(CCFLAGS=["-g1"]) if env["debug_symbols"] == "full": @@ -335,6 +357,12 @@ def configure_mingw(env): env.Append(CCFLAGS=["-g3"]) env.Append(CPPDEFINES=["DEBUG_ENABLED"]) + if env["windows_subsystem"] == "gui": + env.Append(LINKFLAGS=["-Wl,--subsystem,windows"]) + else: + env.Append(LINKFLAGS=["-Wl,--subsystem,console"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) + ## Compiler configuration if os.name != "nt": @@ -424,7 +452,7 @@ def configure_mingw(env): else: env.Append(LIBS=["cfgmgr32"]) - ## TODO !!! Reenable when OpenGLES Rendering Device is implemented !!! + ## TODO !!! Re-enable when OpenGLES Rendering Device is implemented !!! # env.Append(CPPDEFINES=['OPENGL_ENABLED']) env.Append(LIBS=["opengl32"]) diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 6ee9b6d698..dfbb734ee4 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -29,7 +29,9 @@ /*************************************************************************/ #include "display_server_windows.h" + #include "core/io/marshalls.h" +#include "core/math/geometry_2d.h" #include "main/main.h" #include "os_windows.h" #include "scene/resources/texture.h" @@ -42,7 +44,7 @@ static String format_error_message(DWORD id) { size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&messageBuffer, 0, nullptr); - String msg = "Error " + itos(id) + ": " + String(messageBuffer, size); + String msg = "Error " + itos(id) + ": " + String::utf16((const char16_t *)messageBuffer, size); LocalFree(messageBuffer); @@ -78,7 +80,7 @@ String DisplayServerWindows::get_name() const { } void DisplayServerWindows::alert(const String &p_alert, const String &p_title) { - MessageBoxW(nullptr, p_alert.c_str(), p_title.c_str(), MB_OK | MB_ICONEXCLAMATION | MB_TASKMODAL); + MessageBoxW(nullptr, (LPCWSTR)(p_alert.utf16().get_data()), (LPCWSTR)(p_title.utf16().get_data()), MB_OK | MB_ICONEXCLAMATION | MB_TASKMODAL); } void DisplayServerWindows::_set_mouse_mode_impl(MouseMode p_mode) { @@ -177,11 +179,12 @@ void DisplayServerWindows::clipboard_set(const String &p_text) { } EmptyClipboard(); - HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, (text.length() + 1) * sizeof(CharType)); + Char16String utf16 = text.utf16(); + HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, (utf16.length() + 1) * sizeof(WCHAR)); ERR_FAIL_COND_MSG(mem == nullptr, "Unable to allocate memory for clipboard contents."); LPWSTR lptstrCopy = (LPWSTR)GlobalLock(mem); - memcpy(lptstrCopy, text.c_str(), (text.length() + 1) * sizeof(CharType)); + memcpy(lptstrCopy, utf16.get_data(), (utf16.length() + 1) * sizeof(WCHAR)); GlobalUnlock(mem); SetClipboardData(CF_UNICODETEXT, mem); @@ -218,7 +221,7 @@ String DisplayServerWindows::clipboard_get() const { if (mem != nullptr) { LPWSTR ptr = (LPWSTR)GlobalLock(mem); if (ptr != nullptr) { - ret = String((CharType *)ptr); + ret = String::utf16((const char16_t *)ptr); GlobalUnlock(mem); }; }; @@ -493,15 +496,21 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod wd.no_focus = true; } - _update_window_style(window_id); + return window_id; +} + +void DisplayServerWindows::show_window(WindowID p_id) { + WindowData &wd = windows[p_id]; + + if (p_id != MAIN_WINDOW_ID) { + _update_window_style(p_id); + } - ShowWindow(wd.hWnd, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) ? SW_SHOWNOACTIVATE : SW_SHOW); // Show The Window - if (!(p_flags & WINDOW_FLAG_NO_FOCUS_BIT)) { + ShowWindow(wd.hWnd, wd.no_focus ? SW_SHOWNOACTIVATE : SW_SHOW); // Show The Window + if (!wd.no_focus) { SetForegroundWindow(wd.hWnd); // Slightly Higher Priority SetFocus(wd.hWnd); // Sets Keyboard Focus To } - - return window_id; } void DisplayServerWindows::delete_sub_window(WindowID p_window) { @@ -587,7 +596,37 @@ void DisplayServerWindows::window_set_title(const String &p_title, WindowID p_wi _THREAD_SAFE_METHOD_ ERR_FAIL_COND(!windows.has(p_window)); - SetWindowTextW(windows[p_window].hWnd, p_title.c_str()); + SetWindowTextW(windows[p_window].hWnd, (LPCWSTR)(p_title.utf16().get_data())); +} + +void DisplayServerWindows::window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_COND(!windows.has(p_window)); + windows[p_window].mpath = p_region; + _update_window_mouse_passthrough(p_window); +} + +void DisplayServerWindows::_update_window_mouse_passthrough(WindowID p_window) { + if (windows[p_window].mpath.size() == 0) { + SetWindowRgn(windows[p_window].hWnd, nullptr, TRUE); + } else { + POINT *points = (POINT *)memalloc(sizeof(POINT) * windows[p_window].mpath.size()); + for (int i = 0; i < windows[p_window].mpath.size(); i++) { + if (windows[p_window].borderless) { + points[i].x = windows[p_window].mpath[i].x; + points[i].y = windows[p_window].mpath[i].y; + } else { + points[i].x = windows[p_window].mpath[i].x + GetSystemMetrics(SM_CXSIZEFRAME); + points[i].y = windows[p_window].mpath[i].y + GetSystemMetrics(SM_CYSIZEFRAME) + GetSystemMetrics(SM_CYCAPTION); + } + } + + HRGN region = CreatePolygonRgn(points, windows[p_window].mpath.size(), ALTERNATE); + SetWindowRgn(windows[p_window].hWnd, region, TRUE); + DeleteObject(region); + memfree(points); + } } int DisplayServerWindows::window_get_current_screen(WindowID p_window) const { @@ -1003,6 +1042,7 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W case WINDOW_FLAG_BORDERLESS: { wd.borderless = p_enabled; _update_window_style(p_window); + _update_window_mouse_passthrough(p_window); } break; case WINDOW_FLAG_ALWAYS_ON_TOP: { ERR_FAIL_COND_MSG(wd.transient_parent != INVALID_WINDOW_ID && p_enabled, "Transient windows can't become on top"); @@ -1417,13 +1457,13 @@ String DisplayServerWindows::keyboard_get_layout_language(int p_index) const { HKL *layouts = (HKL *)memalloc(layout_count * sizeof(HKL)); GetKeyboardLayoutList(layout_count, layouts); - wchar_t buf[LOCALE_NAME_MAX_LENGTH]; - memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(wchar_t)); + WCHAR buf[LOCALE_NAME_MAX_LENGTH]; + memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR)); LCIDToLocaleName(MAKELCID(LOWORD(layouts[p_index]), SORT_DEFAULT), buf, LOCALE_NAME_MAX_LENGTH, 0); memfree(layouts); - return String(buf).substr(0, 2); + return String::utf16((const char16_t *)buf).substr(0, 2); } String _get_full_layout_name_from_registry(HKL p_layout) { @@ -1431,17 +1471,17 @@ String _get_full_layout_name_from_registry(HKL p_layout) { String ret; HKEY hkey; - wchar_t layout_text[1024]; - memset(layout_text, 0, 1024 * sizeof(wchar_t)); + WCHAR layout_text[1024]; + memset(layout_text, 0, 1024 * sizeof(WCHAR)); - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, (LPCWSTR)id.c_str(), 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS) { + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, (LPCWSTR)(id.utf16().get_data()), 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS) { return ret; } DWORD buffer = 1024; DWORD vtype = REG_SZ; if (RegQueryValueExW(hkey, L"Layout Text", NULL, &vtype, (LPBYTE)layout_text, &buffer) == ERROR_SUCCESS) { - ret = String(layout_text); + ret = String::utf16((const char16_t *)layout_text); } RegCloseKey(hkey); return ret; @@ -1457,15 +1497,15 @@ String DisplayServerWindows::keyboard_get_layout_name(int p_index) const { String ret = _get_full_layout_name_from_registry(layouts[p_index]); // Try reading full name from Windows registry, fallback to locale name if failed (e.g. on Wine). if (ret == String()) { - wchar_t buf[LOCALE_NAME_MAX_LENGTH]; - memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(wchar_t)); + WCHAR buf[LOCALE_NAME_MAX_LENGTH]; + memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR)); LCIDToLocaleName(MAKELCID(LOWORD(layouts[p_index]), SORT_DEFAULT), buf, LOCALE_NAME_MAX_LENGTH, 0); - wchar_t name[1024]; - memset(name, 0, 1024 * sizeof(wchar_t)); + WCHAR name[1024]; + memset(name, 0, 1024 * sizeof(WCHAR)); GetLocaleInfoEx(buf, LOCALE_SLOCALIZEDDISPLAYNAME, (LPWSTR)&name, 1024); - ret = String(name); + ret = String::utf16((const char16_t *)name); } memfree(layouts); @@ -2026,8 +2066,8 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA Ref<InputEventMouseMotion> mm; mm.instance(); mm->set_window_id(window_id); - mm->set_control(GetKeyState(VK_CONTROL) != 0); - mm->set_shift(GetKeyState(VK_SHIFT) != 0); + mm->set_control(GetKeyState(VK_CONTROL) < 0); + mm->set_shift(GetKeyState(VK_SHIFT) < 0); mm->set_alt(alt_mem); mm->set_pressure(windows[window_id].last_pressure); @@ -2169,8 +2209,8 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_tilt(Vector2((float)pen_info.tiltX / 90, (float)pen_info.tiltY / 90)); } - mm->set_control((wParam & MK_CONTROL) != 0); - mm->set_shift((wParam & MK_SHIFT) != 0); + mm->set_control(GetKeyState(VK_CONTROL) < 0); + mm->set_shift(GetKeyState(VK_SHIFT) < 0); mm->set_alt(alt_mem); mm->set_button_mask(last_button_state); @@ -2705,7 +2745,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_DROPFILES: { HDROP hDropInfo = (HDROP)wParam; const int buffsize = 4096; - wchar_t buf[buffsize]; + WCHAR buf[buffsize]; int fcount = DragQueryFileW(hDropInfo, 0xFFFFFFFF, nullptr, 0); @@ -2713,7 +2753,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA for (int i = 0; i < fcount; i++) { DragQueryFileW(hDropInfo, i, buf, buffsize); - String file = buf; + String file = String::utf16((const char16_t *)buf); files.push_back(file); } @@ -3121,9 +3161,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } } - ShowWindow(windows[MAIN_WINDOW_ID].hWnd, SW_SHOW); // Show The Window - SetForegroundWindow(windows[MAIN_WINDOW_ID].hWnd); // Slightly Higher Priority - SetFocus(windows[MAIN_WINDOW_ID].hWnd); // Sets Keyboard Focus To + show_window(MAIN_WINDOW_ID); #if defined(VULKAN_ENABLED) diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 725f9697c5..0fca2589ae 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -323,6 +323,8 @@ private: HWND hWnd; //layered window + Vector<Vector2> mpath; + bool preserve_window_size = false; bool pre_fs_valid = false; RECT pre_fs_rect; @@ -416,6 +418,7 @@ private: void _touch_event(WindowID p_window, bool p_pressed, float p_x, float p_y, int idx); void _update_window_style(WindowID p_window, bool p_repaint = true, bool p_maximized = false); + void _update_window_mouse_passthrough(WindowID p_window); void _update_real_mouse_position(WindowID p_window); @@ -460,6 +463,7 @@ public: virtual Vector<DisplayServer::WindowID> get_window_list() const; virtual WindowID create_sub_window(WindowMode p_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()); + virtual void show_window(WindowID p_window); virtual void delete_sub_window(WindowID p_window); virtual WindowID get_window_at_screen_position(const Point2i &p_position) const; @@ -476,6 +480,7 @@ public: virtual void window_set_drop_files_callback(const Callable &p_callable, WindowID p_window = MAIN_WINDOW_ID); virtual void window_set_title(const String &p_title, WindowID p_window = MAIN_WINDOW_ID); + virtual void window_set_mouse_passthrough(const Vector<Vector2> &p_region, WindowID p_window = MAIN_WINDOW_ID); virtual int window_get_current_screen(WindowID p_window = MAIN_WINDOW_ID) const; virtual void window_set_current_screen(int p_screen, WindowID p_window = MAIN_WINDOW_ID); diff --git a/platform/windows/godot.natvis b/platform/windows/godot.natvis index 593557cc69..90f0b55d0a 100644 --- a/platform/windows/godot.natvis +++ b/platform/windows/godot.natvis @@ -19,7 +19,7 @@ </ArrayItems> </Expand> </Type> - + <Type Name="List<*>"> <Expand> <Item Name="[size]">_data ? (_data->size_cache) : 0</Item> @@ -62,7 +62,7 @@ <DisplayString Condition="type == Variant::POOL_COLOR_ARRAY">{*(PoolColorArray *)_data._mem}</DisplayString> <StringView Condition="type == Variant::STRING && ((String *)(_data._mem))->_cowdata._ptr">((String *)(_data._mem))->_cowdata._ptr,su</StringView> - + <Expand> <Item Name="[value]" Condition="type == Variant::BOOL">_data._bool</Item> <Item Name="[value]" Condition="type == Variant::INT">_data._int</Item> @@ -143,7 +143,7 @@ <Item Name="alpha">a</Item> </Expand> </Type> - + <Type Name="Node" Inheritable="false"> <Expand> <Item Name="Object">(Object*)this</Item> diff --git a/platform/windows/godot_windows.cpp b/platform/windows/godot_windows.cpp index 910059a9fc..add559a717 100644 --- a/platform/windows/godot_windows.cpp +++ b/platform/windows/godot_windows.cpp @@ -146,6 +146,8 @@ int widechar_main(int argc, wchar_t **argv) { argv_utf8[i] = wc_to_utf8(argv[i]); } + TEST_MAIN_PARAM_OVERRIDE(argc, argv_utf8) + Error err = Main::setup(argv_utf8[0], argc - 1, &argv_utf8[1]); if (err != OK) { @@ -186,10 +188,12 @@ int _main() { return result; } -int main(int _argc, char **_argv) { +int main(int argc, char **argv) { + // override the arguments for the test handler / if symbol is provided + // TEST_MAIN_OVERRIDE + // _argc and _argv are ignored // we are going to use the WideChar version of them instead - #ifdef CRASH_HANDLER_EXCEPTION __try { return _main(); diff --git a/platform/windows/joypad_windows.cpp b/platform/windows/joypad_windows.cpp index 65caee3035..d1454c9096 100644 --- a/platform/windows/joypad_windows.cpp +++ b/platform/windows/joypad_windows.cpp @@ -146,8 +146,8 @@ bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) { if (have_device(instance->guidInstance) || num == -1) return false; - d_joypads[joypad_count] = dinput_gamepad(); - dinput_gamepad *joy = &d_joypads[joypad_count]; + d_joypads[num] = dinput_gamepad(); + dinput_gamepad *joy = &d_joypads[num]; const DWORD devtype = (instance->dwDevType & 0xFF); @@ -171,7 +171,7 @@ bool JoypadWindows::setup_dinput_joypad(const DIDEVICEINSTANCE *instance) { WORD version = 0; sprintf_s(uid, "%04x%04x%04x%04x%04x%04x%04x%04x", type, 0, vendor, 0, product, 0, version, 0); - id_to_change = joypad_count; + id_to_change = num; slider_count = 0; joy->di_joy->SetDataFormat(&c_dfDIJoystick2); diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 5b15896b0c..f73516b370 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -84,7 +84,7 @@ static String format_error_message(DWORD id) { size_t size = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&messageBuffer, 0, nullptr); - String msg = "Error " + itos(id) + ": " + String(messageBuffer, size); + String msg = "Error " + itos(id) + ": " + String::utf16((const char16_t *)messageBuffer, size); LocalFree(messageBuffer); @@ -107,15 +107,11 @@ void RedirectIOToConsole() { // set the screen buffer to be big enough to let us scroll text - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), - - &coninfo); + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &coninfo); coninfo.dwSize.Y = MAX_CONSOLE_LINES; - SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), - - coninfo.dwSize); + SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coninfo.dwSize); // redirect unbuffered STDOUT to the console @@ -265,10 +261,10 @@ Error OS_Windows::open_dynamic_library(const String p_path, void *&p_library_han DLL_DIRECTORY_COOKIE cookie = nullptr; if (p_also_set_library_path && has_dll_directory_api) { - cookie = add_dll_directory(path.get_base_dir().c_str()); + cookie = add_dll_directory((LPCWSTR)(path.get_base_dir().utf16().get_data())); } - p_library_handle = (void *)LoadLibraryExW(path.c_str(), nullptr, (p_also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); + p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(path.utf16().get_data()), nullptr, (p_also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); ERR_FAIL_COND_V_MSG(!p_library_handle, ERR_CANT_OPEN, "Can't open dynamic library: " + p_path + ", error: " + format_error_message(GetLastError()) + "."); if (cookie) { @@ -407,7 +403,7 @@ uint64_t OS_Windows::get_ticks_usec() const { String OS_Windows::_quote_command_line_argument(const String &p_text) const { for (int i = 0; i < p_text.size(); i++) { - CharType c = p_text[i]; + char32_t c = p_text[i]; if (c == ' ' || c == '&' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '^' || c == '=' || c == ';' || c == '!' || c == '\'' || c == '+' || c == ',' || c == '`' || c == '~') { return "\"" + p_text + "\""; } @@ -428,7 +424,7 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, // Note: _wpopen is calling command as "cmd.exe /c argss", instead of executing it directly, add extra quotes around full command, to prevent it from stripping quotes in the command. argss = _quote_command_line_argument(argss); - FILE *f = _wpopen(argss.c_str(), L"r"); + FILE *f = _wpopen((LPCWSTR)(argss.utf16().get_data()), L"r"); ERR_FAIL_COND_V(!f, ERR_CANT_OPEN); char buf[65535]; @@ -463,13 +459,8 @@ Error OS_Windows::execute(const String &p_path, const List<String> &p_arguments, ZeroMemory(&pi.pi, sizeof(pi.pi)); LPSTARTUPINFOW si_w = (LPSTARTUPINFOW)&pi.si; - Vector<CharType> modstr; // Windows wants to change this no idea why. - modstr.resize(cmdline.size()); - for (int i = 0; i < cmdline.size(); i++) { - modstr.write[i] = cmdline[i]; - } - - int ret = CreateProcessW(nullptr, modstr.ptrw(), nullptr, nullptr, 0, NORMAL_PRIORITY_CLASS & CREATE_NO_WINDOW, nullptr, nullptr, si_w, &pi.pi); + Char16String modstr = cmdline.utf16(); // Windows wants to change this no idea why. + int ret = CreateProcessW(nullptr, (LPWSTR)(modstr.ptrw()), nullptr, nullptr, 0, NORMAL_PRIORITY_CLASS & CREATE_NO_WINDOW, nullptr, nullptr, si_w, &pi.pi); ERR_FAIL_COND_V(ret == 0, ERR_CANT_FORK); if (p_blocking) { @@ -509,26 +500,26 @@ int OS_Windows::get_process_id() const { } Error OS_Windows::set_cwd(const String &p_cwd) { - if (_wchdir(p_cwd.c_str()) != 0) + if (_wchdir((LPCWSTR)(p_cwd.utf16().get_data())) != 0) return ERR_CANT_OPEN; return OK; } String OS_Windows::get_executable_path() const { - wchar_t bufname[4096]; + WCHAR bufname[4096]; GetModuleFileNameW(nullptr, bufname, 4096); - String s = bufname; + String s = String::utf16((const char16_t *)bufname); return s; } bool OS_Windows::has_environment(const String &p_var) const { #ifdef MINGW_ENABLED - return _wgetenv(p_var.c_str()) != nullptr; + return _wgetenv((LPCWSTR)(p_var.utf16().get_data())) != nullptr; #else - wchar_t *env; + WCHAR *env; size_t len; - _wdupenv_s(&env, &len, p_var.c_str()); + _wdupenv_s(&env, &len, (LPCWSTR)(p_var.utf16().get_data())); const bool has_env = env != nullptr; free(env); return has_env; @@ -536,16 +527,16 @@ bool OS_Windows::has_environment(const String &p_var) const { }; String OS_Windows::get_environment(const String &p_var) const { - wchar_t wval[0x7Fff]; // MSDN says 32767 char is the maximum - int wlen = GetEnvironmentVariableW(p_var.c_str(), wval, 0x7Fff); + WCHAR wval[0x7fff]; // MSDN says 32767 char is the maximum + int wlen = GetEnvironmentVariableW((LPCWSTR)(p_var.utf16().get_data()), wval, 0x7fff); if (wlen > 0) { - return wval; + return String::utf16((const char16_t *)wval); } return ""; } bool OS_Windows::set_environment(const String &p_var, const String &p_value) const { - return (bool)SetEnvironmentVariableW(p_var.c_str(), p_value.c_str()); + return (bool)SetEnvironmentVariableW((LPCWSTR)(p_var.utf16().get_data()), (LPCWSTR)(p_value.utf16().get_data())); } String OS_Windows::get_stdin_string(bool p_block) { @@ -558,7 +549,7 @@ String OS_Windows::get_stdin_string(bool p_block) { } Error OS_Windows::shell_open(String p_uri) { - ShellExecuteW(nullptr, nullptr, p_uri.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + ShellExecuteW(nullptr, nullptr, (LPCWSTR)(p_uri.utf16().get_data()), nullptr, nullptr, SW_SHOWNORMAL); return OK; } @@ -701,7 +692,7 @@ String OS_Windows::get_system_dir(SystemDir p_dir) const { PWSTR szPath; HRESULT res = SHGetKnownFolderPath(id, 0, nullptr, &szPath); ERR_FAIL_COND_V(res != S_OK, String()); - String path = String(szPath); + String path = String::utf16((const char16_t *)szPath); CoTaskMemFree(szPath); return path; } @@ -727,7 +718,7 @@ String OS_Windows::get_user_data_dir() const { String OS_Windows::get_unique_id() const { HW_PROFILE_INFO HwProfInfo; ERR_FAIL_COND_V(!GetCurrentHwProfile(&HwProfInfo), ""); - return String(HwProfInfo.szHwProfileGuid); + return String::utf16((const char16_t *)(HwProfInfo.szHwProfileGuid), HW_PROFILE_GUIDLEN); } bool OS_Windows::_check_internal_feature_support(const String &p_feature) { @@ -744,9 +735,11 @@ bool OS_Windows::is_disable_crash_handler() const { Error OS_Windows::move_to_trash(const String &p_path) { SHFILEOPSTRUCTW sf; - WCHAR *from = new WCHAR[p_path.length() + 2]; - wcscpy_s(from, p_path.length() + 1, p_path.c_str()); - from[p_path.length() + 1] = 0; + + Char16String utf16 = p_path.utf16(); + WCHAR *from = new WCHAR[utf16.length() + 2]; + wcscpy_s(from, utf16.length() + 1, (LPCWSTR)(utf16.get_data())); + from[utf16.length() + 1] = 0; sf.hwnd = main_window; sf.wFunc = FO_DELETE; diff --git a/platform/windows/platform_config.h b/platform/windows/platform_config.h index 290decac5f..09a16614e0 100644 --- a/platform/windows/platform_config.h +++ b/platform/windows/platform_config.h @@ -29,5 +29,3 @@ /*************************************************************************/ #include <malloc.h> - -#define GLES2_INCLUDE_H "thirdparty/glad/glad/glad.h" |