diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c8144d78a..6ada91a73f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,15 @@ endif() PROJECT(FS2_Open LANGUAGES ${FSO_LANGUAGES}) +if (ANDROID) + message(STATUS "Building for Android (NDK)") + set(FSO_BUILD_INCLUDED_LIBS ON CACHE BOOL "" FORCE) + set(USING_PREBUILT_LIBS ON CACHE BOOL "" FORCE) + set(SDL3_USE_PRECOMPILED ON CACHE BOOL "" FORCE) + set(OPENAL_USE_PRECOMPILED ON CACHE BOOL "" FORCE) + set(USING_PREBUILT_VULKAN ON CACHE BOOL "" FORCE) +endif() + # Check if the external modules exists IF(NOT IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/lib/libRocket/Build") message(FATAL_ERROR "External submodules could not be found. Please make sure you have updated your submodules.") @@ -109,8 +118,8 @@ OPTION(FSO_BUILD_TESTS "Build unit tests" OFF) OPTION(FSO_DEVELOPMENT_MODE "Generate binaries in development mode, only use if you know what you're doing!" OFF) -IF(WIN32 OR APPLE) - # On windows and mac the default should be to always build the included libraries +IF(WIN32 OR APPLE OR ANDROID) + # On windows, mac and android the default should be to always build the included libraries SET(FSO_BUILD_INCLUDED_LIBS_DEFAULT ON) ELSE() SET(FSO_BUILD_INCLUDED_LIBS_DEFAULT OFF) diff --git a/code/cfile/cfile.cpp b/code/cfile/cfile.cpp index f4868f104f..6fac45be3c 100644 --- a/code/cfile/cfile.cpp +++ b/code/cfile/cfile.cpp @@ -185,7 +185,12 @@ int cfile_init(const char *exe_dir, const char *cdrom_dir) char buf[CFILE_ROOT_DIRECTORY_LEN]; - strncpy(buf, exe_dir, CFILE_ROOT_DIRECTORY_LEN - 1); + #ifndef __ANDROID__ + strncpy(buf, exe_dir, CFILE_ROOT_DIRECTORY_LEN - 1); + #else + (void)exe_dir; + strncpy(buf, os_android_get_working_folder_path().c_str(), CFILE_ROOT_DIRECTORY_LEN - 1); + #endif buf[CFILE_ROOT_DIRECTORY_LEN - 1] = '\0'; diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index 1351c72f1d..c67e7e581c 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -75,6 +75,7 @@ const char *cmdline_arg_types[] = enum class flag_output_type { Binary, Json_V1, + Json_V2, }; // variables @@ -555,7 +556,12 @@ int Cmdline_show_pos = 0; int Cmdline_show_stats = 0; int Cmdline_save_render_targets = 0; bool Cmdline_window = false; +#ifdef __ANDROID__ +// Default to borderless in the legacy window-mode path as well. +bool Cmdline_fullscreen_window = true; +#else bool Cmdline_fullscreen_window = false; +#endif std::optional>Cmdline_window_res = std::nullopt; char *Cmdline_res = 0; char *Cmdline_center_res = 0; @@ -1358,51 +1364,58 @@ static void handle_unix_modlist(char **modlist, size_t *len) // external entry point into this modules +static void json_add_version(json_t* root) { + auto version_obj = json_object(); + + json_object_set_new(version_obj, "full", json_string(FS_VERSION_FULL)); + json_object_set_new(version_obj, "major", json_integer(FS_VERSION_MAJOR)); + json_object_set_new(version_obj, "minor", json_integer(FS_VERSION_MINOR)); + json_object_set_new(version_obj, "build", json_integer(FS_VERSION_BUILD)); + + json_object_set_new(version_obj, "has_revision", json_boolean(FS_VERSION_HAS_REVIS)); + json_object_set_new(version_obj, "revision", json_integer(FS_VERSION_REVIS)); + json_object_set_new(version_obj, "revision_str", json_string(FS_VERSION_REVIS_STR)); + + json_object_set_new(root, "version", version_obj); +} + +static void json_add_easy_flags(json_t* root) { + auto easy_array = json_array(); + + for (auto& easy_flag : easy_flags) { + json_array_append_new(easy_array, json_string(easy_flag.name)); + } + + json_object_set_new(root, "easy_flags", easy_array); +} + +static void json_add_flags(json_t* root) { + auto flags_array = json_array(); + + for (auto& flag : exe_params) { + auto flag_obj = json_object(); + + json_object_set_new(flag_obj, "name", json_string(flag.name)); + json_object_set_new(flag_obj, "description", json_string(flag.desc)); + json_object_set_new(flag_obj, "fso_only", json_boolean(flag.fso_only)); + json_object_set_new(flag_obj, "on_flags", json_integer(flag.on_flags)); + json_object_set_new(flag_obj, "off_flags", json_integer(flag.off_flags)); + json_object_set_new(flag_obj, "type", json_string(flag.type)); + json_object_set_new(flag_obj, "web_url", json_string(flag.web_url)); + + json_array_append_new(flags_array, flag_obj); + } + + json_object_set_new(root, "flags", flags_array); +} + static json_t* json_get_v1() { auto root = json_object(); - { - auto version_obj = json_object(); + json_add_version(root); + json_add_easy_flags(root); + json_add_flags(root); - json_object_set_new(version_obj, "full", json_string(FS_VERSION_FULL)); - json_object_set_new(version_obj, "major", json_integer(FS_VERSION_MAJOR)); - json_object_set_new(version_obj, "minor", json_integer(FS_VERSION_MINOR)); - json_object_set_new(version_obj, "build", json_integer(FS_VERSION_BUILD)); - - json_object_set_new(version_obj, "has_revision", json_boolean(FS_VERSION_HAS_REVIS)); - json_object_set_new(version_obj, "revision", json_integer(FS_VERSION_REVIS)); - json_object_set_new(version_obj, "revision_str", json_string(FS_VERSION_REVIS_STR)); - - json_object_set_new(root, "version", version_obj); - } - { - auto easy_array = json_array(); - - for (auto& easy_flag : easy_flags) { - json_array_append_new(easy_array, json_string(easy_flag.name)); - } - - json_object_set_new(root, "easy_flags", easy_array); - } - { - auto flags_array = json_array(); - - for (auto& flag : exe_params) { - auto flag_obj = json_object(); - - json_object_set_new(flag_obj, "name", json_string(flag.name)); - json_object_set_new(flag_obj, "description", json_string(flag.desc)); - json_object_set_new(flag_obj, "fso_only", json_boolean(flag.fso_only)); - json_object_set_new(flag_obj, "on_flags", json_integer(flag.on_flags)); - json_object_set_new(flag_obj, "off_flags", json_integer(flag.off_flags)); - json_object_set_new(flag_obj, "type", json_string(flag.type)); - json_object_set_new(flag_obj, "web_url", json_string(flag.web_url)); - - json_array_append_new(flags_array, flag_obj); - } - - json_object_set_new(root, "flags", flags_array); - } { auto caps_array = json_array(); @@ -1513,6 +1526,19 @@ static json_t* json_get_v1() { return root; } +// json_v2 reports version, supported flags, and the preferences path for launchers, +// without querying graphics, audio, or joystick devices. +static json_t* json_get_v2() { + auto root = json_object(); + + json_add_version(root); + json_add_easy_flags(root); + json_add_flags(root); + json_object_set_new(root, "pref_path", json_string(os_get_config_path().c_str())); + + return root; +} + static void write_flags_file() { FILE *fp = fopen("flags.lch","w"); @@ -1570,6 +1596,8 @@ static flag_output_type get_flags_output_type() { return flag_output_type::Binary; } else if (type == "json_v1") { return flag_output_type::Json_V1; + } else if (type == "json_v2") { + return flag_output_type::Json_V2; } else { // This is supposed to make it easy for the launcher to recognize an unsupported type printf("OUTPUT TYPE NOT SUPPORTED!\n"); @@ -1580,17 +1608,23 @@ static flag_output_type get_flags_output_type() { static void write_flags() { auto type = get_flags_output_type(); - switch(type) { - case flag_output_type::Binary: - write_flags_file(); - break; - case flag_output_type::Json_V1: - json_t* root = json_get_v1(); - json_dumpf(root, stdout, JSON_INDENT(4)); - json_decref(root); - break; + if (type == flag_output_type::Binary) { + write_flags_file(); + return; } + + json_t* root = (type == flag_output_type::Json_V1) ? json_get_v1() : json_get_v2(); + +#ifdef __ANDROID__ + char* dumped = json_dumps(root, JSON_INDENT(4)); + os_android_set_flags_string(dumped ? dumped : ""); + free(dumped); +#else + json_dumpf(root, stdout, JSON_INDENT(4)); +#endif + + json_decref(root); } bool SetCmdlineParams() @@ -1787,6 +1821,7 @@ bool SetCmdlineParams() options::OptionsManager::instance()->setOverride("Graphics.WindowMode", std::unique_ptr(json_integer(static_cast(os::ViewportState::Windowed))), "-window"); Cmdline_window = true; + Cmdline_fullscreen_window = false; } if ( fullscreen_window_arg.found( ) ) diff --git a/code/external_dll/externalcode.h b/code/external_dll/externalcode.h index 5dd2f0f597..1f6ddee84d 100644 --- a/code/external_dll/externalcode.h +++ b/code/external_dll/externalcode.h @@ -28,6 +28,23 @@ protected: return FALSE; m_library = SDL_LoadObject(externlib); + +#ifdef __ANDROID__ + if (m_library == NULL) { + auto android_path = SDL_GetAndroidInternalStoragePath(); + if (android_path != nullptr) { + SCP_string android_full_path = android_path; + if (android_full_path.back() != DIR_SEPARATOR_CHAR) { + android_full_path += DIR_SEPARATOR_CHAR; + } + android_full_path += SCP_string("natives") + DIR_SEPARATOR_CHAR + externlib; + #ifndef NDEBUG + mprintf(("Calling SDL_LoadObject with the following path: '%s'\n", android_full_path.c_str())); + #endif + m_library = SDL_LoadObject(android_full_path.c_str()); + } + } +#endif // check full path as a fallback for our own libraries // NOTE: basePath is assumed to have a trailing slash! diff --git a/code/graphics/2d.cpp b/code/graphics/2d.cpp index 76e705b2aa..080c143a8a 100644 --- a/code/graphics/2d.cpp +++ b/code/graphics/2d.cpp @@ -289,7 +289,11 @@ GraphicsAPI gr_get_configured_render_api() #endif +#ifndef __ANDROID__ os::ViewportState Gr_configured_window_state = os::ViewportState::Fullscreen; +#else +os::ViewportState Gr_configured_window_state = os::ViewportState::Borderless; +#endif static bool mode_change_func(os::ViewportState state, bool initial) { diff --git a/code/graphics/opengl/gropengl.cpp b/code/graphics/opengl/gropengl.cpp index 509406d4cd..fb202673b5 100644 --- a/code/graphics/opengl/gropengl.cpp +++ b/code/graphics/opengl/gropengl.cpp @@ -6,7 +6,7 @@ #include #endif -#if !defined __APPLE_CC__ && defined SCP_UNIX +#if !defined __APPLE_CC__ && !defined __ANDROID__ && defined SCP_UNIX #include //Required because X defines none and always, which is used later #undef None @@ -1394,7 +1394,7 @@ bool gr_opengl_init(std::unique_ptr&& graphicsOps) Error(LOCATION, "Failed to load OpenGL!"); } -#if !defined __APPLE_CC__ && defined SCP_UNIX +#if !defined __APPLE_CC__ && !defined __ANDROID__ && defined SCP_UNIX if (!gladLoadGLXLoader(GL_context->getLoaderFunction(), nullptr, 0)) { Error(LOCATION, "Failed to load GLX!"); } diff --git a/code/graphics/vulkan/VulkanRenderer.h b/code/graphics/vulkan/VulkanRenderer.h index b0e05b65a0..00924cdc7b 100644 --- a/code/graphics/vulkan/VulkanRenderer.h +++ b/code/graphics/vulkan/VulkanRenderer.h @@ -456,6 +456,7 @@ class VulkanRenderer { void acquireNextSwapChainImage(); bool recreateSwapChain(); + bool shouldRecreateSwapChain(SwapChainStatus status) const; void createImGuiDescriptorPool(); void initImGui(); @@ -480,6 +481,7 @@ class VulkanRenderer { bool m_hdrActive = false; // True when an HDR10 (PQ/BT.2020) swap chain was negotiated bool m_hdrMetadataSupported = false; // VK_EXT_hdr_metadata device extension enabled vk::Extent2D m_swapChainExtent; + vk::SurfaceTransformFlagBitsKHR m_swapChainPreTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity; // The resolution the engine draws at (gr_screen.max_w/max_h), which is not // always the window size: when Cmdline_window_res is set, // SDLGraphicsOperations::createViewport sizes the window from that instead, diff --git a/code/graphics/vulkan/VulkanRendererLoop.cpp b/code/graphics/vulkan/VulkanRendererLoop.cpp index 58324d4265..700524f869 100644 --- a/code/graphics/vulkan/VulkanRendererLoop.cpp +++ b/code/graphics/vulkan/VulkanRendererLoop.cpp @@ -81,7 +81,7 @@ void VulkanRenderer::acquireNextSwapChainImage() MAX_ACQUIRE_ATTEMPTS); } - if (status == SwapChainStatus::eSuboptimal) { + if (shouldRecreateSwapChain(status)) { m_swapChainNeedsRecreation = true; } @@ -221,7 +221,7 @@ void VulkanRenderer::flip() auto presentStatus = m_frames[m_currentFrame]->submitAndPresent(m_currentCommandBuffers, m_renderingFinishedSemaphores[m_currentSwapChainImage].get()); - if (presentStatus == SwapChainStatus::eSuboptimal || presentStatus == SwapChainStatus::eOutOfDate) { + if (shouldRecreateSwapChain(presentStatus)) { m_swapChainNeedsRecreation = true; } diff --git a/code/graphics/vulkan/VulkanRendererSetup.cpp b/code/graphics/vulkan/VulkanRendererSetup.cpp index 80dc67b294..1dc3bdddb3 100644 --- a/code/graphics/vulkan/VulkanRendererSetup.cpp +++ b/code/graphics/vulkan/VulkanRendererSetup.cpp @@ -326,17 +326,17 @@ vk::Extent2D windowExtent() return {static_cast(gr_screen.max_w), static_cast(gr_screen.max_h)}; } -vk::Extent2D chooseSwapChainExtent(const PhysicalDeviceValues& values, uint32_t width, uint32_t height) +vk::Extent2D chooseSwapChainExtent(const vk::SurfaceCapabilitiesKHR& capabilities, uint32_t width, uint32_t height) { - if (values.surfaceCapabilities.currentExtent.width != UINT32_MAX) { - return values.surfaceCapabilities.currentExtent; + if (capabilities.currentExtent.width != UINT32_MAX) { + return capabilities.currentExtent; } else { VkExtent2D actualExtent = {width, height}; - actualExtent.width = std::max(values.surfaceCapabilities.minImageExtent.width, - std::min(values.surfaceCapabilities.maxImageExtent.width, actualExtent.width)); - actualExtent.height = std::max(values.surfaceCapabilities.minImageExtent.height, - std::min(values.surfaceCapabilities.maxImageExtent.height, actualExtent.height)); + actualExtent.width = std::max(capabilities.minImageExtent.width, + std::min(capabilities.maxImageExtent.width, actualExtent.width)); + actualExtent.height = std::max(capabilities.minImageExtent.height, + std::min(capabilities.maxImageExtent.height, actualExtent.height)); return actualExtent; } @@ -1113,7 +1113,7 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v createInfo.minImageCount = imageCount; createInfo.imageFormat = surfaceFormat.format; createInfo.imageColorSpace = surfaceFormat.colorSpace; - createInfo.imageExtent = chooseSwapChainExtent(deviceValues, window.width, window.height); + createInfo.imageExtent = chooseSwapChainExtent(deviceValues.surfaceCapabilities, window.width, window.height); createInfo.imageArrayLayers = 1; createInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc @@ -1128,7 +1128,19 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v createInfo.imageSharingMode = vk::SharingMode::eExclusive; } - createInfo.preTransform = deviceValues.surfaceCapabilities.currentTransform; + // preTransform describes the transform already applied to the rendered content. + // FSO does not pre-rotate it, so request identity whenever supported. If this + // differs from currentTransform (e.g. landscape on an Android phone), the + // presentation engine compensates during presentation. Passing currentTransform + // instead would claim that FSO had already performed that rotation. + auto supported = deviceValues.surfaceCapabilities.supportedTransforms; + if (supported & vk::SurfaceTransformFlagBitsKHR::eIdentity) { + createInfo.preTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity; + } + else { + createInfo.preTransform = deviceValues.surfaceCapabilities.currentTransform; + } + createInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque; createInfo.presentMode = choosePresentMode(deviceValues); createInfo.clipped = true; @@ -1149,6 +1161,7 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v m_hdrActive = (surfaceFormat.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT); Gr_hdr_output_active = m_hdrActive; m_swapChainExtent = createInfo.imageExtent; + m_swapChainPreTransform = createInfo.preTransform; // Without -window_res the render resolution simply is the window, so follow // gr_screen (which gr_screen_resize() has already updated for this @@ -1210,6 +1223,42 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v return true; } +bool VulkanRenderer::shouldRecreateSwapChain(SwapChainStatus status) const +{ + // An already requested rebuild also avoids redundant Android surface queries. + if (m_swapChainNeedsRecreation || status == SwapChainStatus::eOutOfDate) { + return true; + } + if (status != SwapChainStatus::eSuboptimal) { + return false; + } + +#ifdef __ANDROID__ + if (m_swapChainPreTransform == vk::SurfaceTransformFlagBitsKHR::eIdentity) { + vk::SurfaceCapabilitiesKHR capabilities; + try { + capabilities = m_physicalDevice.getSurfaceCapabilitiesKHR(m_vkSurface.get()); + } catch (const vk::SystemError&) { + // Let the normal recreation path handle a failed surface query. + return true; + } + if (capabilities.currentTransform != m_swapChainPreTransform && + (capabilities.supportedTransforms & m_swapChainPreTransform)) { + const auto window = windowExtent(); + const auto extent = chooseSwapChainExtent(capabilities, window.width, window.height); + if (extent == m_swapChainExtent) { + // Android can report SUBOPTIMAL on every present while the compositor + // compensates for our identity preTransform. Rebuilding with the same + // extent and preTransform would just repeat that condition next frame. + return false; + } + } + } +#endif + + return true; +} + bool VulkanRenderer::recreateSwapChain() { nprintf(("vulkan", "Vulkan: Recreating swap chain...\n")); @@ -1233,7 +1282,7 @@ bool VulkanRenderer::recreateSwapChain() // Check for 0x0 extent (minimized window) — caller should retry later const vk::Extent2D window = windowExtent(); - auto extent = chooseSwapChainExtent(freshValues, window.width, window.height); + auto extent = chooseSwapChainExtent(freshValues.surfaceCapabilities, window.width, window.height); if (extent.width == 0 || extent.height == 0) { nprintf(("vulkan", "Vulkan: Surface extent is 0x0 (minimized), deferring swap chain recreation\n")); return false; diff --git a/code/localization/localize.cpp b/code/localization/localize.cpp index 78868daf55..978d9fcd86 100644 --- a/code/localization/localize.cpp +++ b/code/localization/localize.cpp @@ -65,7 +65,7 @@ bool *Lcl_unexpected_tstring_check = nullptr; // NOTE: with map storage of XSTR strings, the indexes no longer need to be contiguous, // but internal strings should still increment XSTR_SIZE to avoid collisions. // retail XSTR_SIZE = 1570 -// #define XSTR_SIZE 1936 // This is the next available ID +// #define XSTR_SIZE 1940 // This is the next available ID // struct to allow for strings.tbl-determined x offset // offset is 0 for english, by default diff --git a/code/osapi/osapi.cpp b/code/osapi/osapi.cpp index 304314cfe4..8fb5fb4c94 100644 --- a/code/osapi/osapi.cpp +++ b/code/osapi/osapi.cpp @@ -30,6 +30,14 @@ #include #endif +#ifdef __ANDROID__ +#include +#include +#include +#include "options/Option.h" +#include "utils/finally.h" +#endif + namespace { const char* ORGANIZATION_NAME = "HardLightProductions"; @@ -925,3 +933,175 @@ SCP_string os_get_config_path(const SCP_string& subpath) return ss.str(); } +/* + Special functions for Android +*/ + +#ifdef __ANDROID__ +static bool android_check_exception(JNIEnv* env, const char* operation) +{ + if (!env->ExceptionCheck()) { + return false; + } + + mprintf(("OSAPI: JNI exception in %s.\n", operation)); + env->ExceptionDescribe(); + env->ExceptionClear(); + return true; +} + +template +static void android_with_static_method(const char* name, const char* signature, Callback&& callback) +{ + JNIEnv* env = static_cast(SDL_GetAndroidJNIEnv()); + if (env == nullptr) { + mprintf(("OSAPI: Couldn't get JNI environment for %s.\n", name)); + return; + } + if (android_check_exception(env, name)) { + return; + } + + if (env->PushLocalFrame(4) < 0) { + android_check_exception(env, "PushLocalFrame"); + mprintf(("OSAPI: Couldn't create JNI local frame for %s.\n", name)); + return; + } + // Release the activity, class, and temporary strings on every exit path. + auto local_frame = util::finally([env]() { env->PopLocalFrame(nullptr); }); + + jobject activity = static_cast(SDL_GetAndroidActivity()); + if (android_check_exception(env, "SDL_GetAndroidActivity") || activity == nullptr) { + mprintf(("OSAPI: Couldn't get Android activity for %s.\n", name)); + return; + } + + jclass activity_class = env->GetObjectClass(activity); + if (android_check_exception(env, "GetObjectClass") || activity_class == nullptr) { + mprintf(("OSAPI: Couldn't get Android activity class for %s.\n", name)); + return; + } + + jmethodID method = env->GetStaticMethodID(activity_class, name, signature); + if (android_check_exception(env, name) || method == nullptr) { + mprintf(("OSAPI: Couldn't find static method %s with signature %s.\n", name, signature)); + return; + } + + callback(env, activity_class, method); + android_check_exception(env, name); +} + +void os_android_set_flags_string(const char* json) +{ + android_with_static_method("setFlagsJson", "(Ljava/lang/String;)V", + [json](JNIEnv* env, jclass activity_class, jmethodID method) { + jstring value = env->NewStringUTF(json ? json : ""); + if (android_check_exception(env, "NewStringUTF") || value == nullptr) { + return; + } + env->CallStaticVoidMethod(activity_class, method, value); + }); +} + +SCP_string os_android_get_working_folder_path() +{ + SCP_string wfp{}; + android_with_static_method("getWorkingFolder", "()Ljava/lang/String;", + [&wfp](JNIEnv* env, jclass activity_class, jmethodID method) { + jstring value = static_cast(env->CallStaticObjectMethod(activity_class, method)); + if (android_check_exception(env, "getWorkingFolder") || value == nullptr) { + return; + } + + const char* working_folder = env->GetStringUTFChars(value, nullptr); + if (android_check_exception(env, "GetStringUTFChars") || working_folder == nullptr) { + return; + } + auto release_chars = util::finally([env, value, working_folder]() { + env->ReleaseStringUTFChars(value, working_folder); + }); + wfp = working_folder; + }); + + if (wfp.empty()) { + mprintf(("Couldn't get working folder path from Java class, reverting to SDL default.\n")); + // SDL's external storage path already includes the app's files directory. + const char* fallbackPath = SDL_GetAndroidExternalStoragePath(); + if (fallbackPath) { + wfp = SCP_string(fallbackPath); + } + } + + // Ensure path ends with a dir separator + if (!wfp.empty() && (wfp.back() != DIR_SEPARATOR_CHAR)) { + wfp += DIR_SEPARATOR_CHAR; + } + mprintf(("Using working folder: %s\n", wfp.c_str())); + return wfp; +} + +void os_android_touch_overlay_toggle(bool status) +{ + android_with_static_method(status ? "enableOverlay" : "disableOverlay", "()V", + [](JNIEnv* env, jclass activity_class, jmethodID method) { + env->CallStaticVoidMethod(activity_class, method); + }); +} + +void os_android_touch_overlay_set_opacity(int opacity) +{ + android_with_static_method("setOverlayOpacity", "(F)V", + [opacity](JNIEnv* env, jclass activity_class, jmethodID method) { + env->CallStaticVoidMethod(activity_class, method, static_cast(opacity) / 100.0f); + }); +} + +static bool touch_ui_change(bool new_val, bool initial) +{ + if (initial) { + return false; + } + os_android_touch_overlay_toggle(new_val); + return true; +} + +static auto TouchOverlayOption = options::OptionBuilder("Input.TouchOverlay", + std::pair{"Touch Overlay", 1936}, + std::pair{"Enable or disable the touch overlay", 1937}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .change_listener(touch_ui_change) + .default_val(true) + .importance(2) + .finish(); + +static bool touch_ui_opacity_change(int new_val, bool initial) +{ + Assertion(new_val >= 0 && new_val <= 100, "Invalid value %d supplied by options system!", new_val); + if (initial) { + return false; + } + os_android_touch_overlay_set_opacity(new_val); + return true; +} + +static auto TouchOverlayOpacityOption = options::OptionBuilder("Input.TouchOverlayOpacity", + std::pair{"Touch Overlay Opacity", 1938}, + std::pair{"Set the opacity of the touch overlay", 1939}) + .category(std::make_pair("Input", 1827)) + .level(options::ExpertLevel::Beginner) + .range(0, 100) + .default_val(20) + .flags({ options::OptionFlags::RangeTypeInteger }) + .change_listener(touch_ui_opacity_change) + .importance(1) + .finish(); + +void os_android_touch_overlay_init() +{ + os_android_touch_overlay_toggle(TouchOverlayOption->getValue()); + os_android_touch_overlay_set_opacity(TouchOverlayOpacityOption->getValue()); +} +#endif + diff --git a/code/osapi/osapi.h b/code/osapi/osapi.h index a81810e0c9..10c9deb932 100644 --- a/code/osapi/osapi.h +++ b/code/osapi/osapi.h @@ -90,6 +90,23 @@ bool os_is_legacy_mode(); */ SCP_string os_get_config_path(const SCP_string& subpath = ""); +/* + Special functions for Android +*/ +#ifdef __ANDROID__ +// Get working folder absolute path from Android Java Class +SCP_string os_android_get_working_folder_path(); + +// Calls to display the touch overlay depending on its last state +void os_android_touch_overlay_init(); + +// Enable or disable the touch UI overlay +void os_android_touch_overlay_toggle(bool status); + +// Send the JSON flags string to the Android Java layer +void os_android_set_flags_string(const char* json); +#endif + namespace os { /** diff --git a/code/sound/openal.h b/code/sound/openal.h index cd77a1bb29..ddf32509e6 100644 --- a/code/sound/openal.h +++ b/code/sound/openal.h @@ -3,7 +3,7 @@ #define _AL_H -#if defined(__APPLE__) +#if defined(__APPLE__) || defined(__ANDROID__) #include "al.h" #include "alc.h" #else diff --git a/code/sound/speech_android.cpp b/code/sound/speech_android.cpp index 6a5a2b5e0c..2f68d4d4ff 100644 --- a/code/sound/speech_android.cpp +++ b/code/sound/speech_android.cpp @@ -4,8 +4,8 @@ #include "utils/unicode.h" #include "speech.h" #include -#include "SDL.h" -#include "SDL_system.h" +#include +#include bool Speech_init = false; static jclass j_game_class = nullptr; @@ -41,8 +41,8 @@ bool speech_init() mprintf(("Speech : Try to init TTSManager on GameActivity...\n")); // Get the JNI Environment pointer and current Activity instance via SDL - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); - jobject activity = (jobject)SDL_AndroidGetActivity(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); + jobject activity = (jobject)SDL_GetAndroidActivity(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); @@ -101,7 +101,7 @@ bool speech_play(const SCP_string& text) return false; } - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); return false; @@ -123,7 +123,7 @@ bool speech_stop() if (!Speech_init) return false; - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); return false; @@ -137,7 +137,7 @@ bool speech_pause() if (!Speech_init) return false; - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); return false; @@ -151,7 +151,7 @@ bool speech_resume() if (!Speech_init) return false; - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); return false; @@ -165,7 +165,7 @@ bool speech_is_speaking() if (!Speech_init) return false; - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); return false; @@ -179,7 +179,7 @@ void speech_deinit() if (!Speech_init) return; - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env != nullptr) { env->CallStaticVoidMethod(j_game_class, tts_shutdown); env->DeleteGlobalRef(j_game_class); @@ -217,7 +217,7 @@ bool speech_set_rate(float rate_percent) else if(rate_percent < 50.0) rate_percent = 50.0; - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); return false; @@ -233,7 +233,7 @@ bool speech_set_voice(int voice) if (!Speech_init) return false; - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); return false; @@ -263,7 +263,7 @@ SCP_vector> speech_enumerate_voices() if (!Speech_init) return voices; - JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv(); + JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv(); if (env == nullptr) { mprintf(("Speech : Unable to get JNI environment!\n")); return voices; diff --git a/code/windows_stub/stubs.cpp b/code/windows_stub/stubs.cpp index 1f45959c7e..df4ae39b61 100644 --- a/code/windows_stub/stubs.cpp +++ b/code/windows_stub/stubs.cpp @@ -49,6 +49,7 @@ int filelength(int fd) SCP_string dump_stacktrace() { +#ifndef __ANDROID__ #ifdef SCP_HAVE_EXECINFO_H // The following is adapted from here: https://panthema.net/2008/0901-stacktrace-demangled/ const int ADDR_SIZE = 64; @@ -139,6 +140,9 @@ SCP_string dump_stacktrace() #else return "No stacktrace available"; #endif +#else +return "No stacktrace available"; +#endif } // retrieve the current working directory diff --git a/freespace2/CMakeLists.txt b/freespace2/CMakeLists.txt index ed83791eaf..8c52e604b8 100644 --- a/freespace2/CMakeLists.txt +++ b/freespace2/CMakeLists.txt @@ -19,7 +19,17 @@ IF(MSVC60) link_directories(${STLPORT_INCLUDE_LIB_DIRS}) ENDIF(MSVC60) -ADD_EXECUTABLE(Freespace2 ${EXE_GUI_TYPE} ${FREESPACE_SRC}) +# For android, build a library not a executable +if (ANDROID) + add_library(Freespace2 SHARED ${FREESPACE_SRC}) + + # NDK base libs + find_library(log-lib log) + find_library(android-lib android) + target_link_libraries(Freespace2 PRIVATE ${log-lib} ${android-lib}) +else() + add_executable(Freespace2 ${EXE_GUI_TYPE} ${FREESPACE_SRC}) +endif() target_compile_features(Freespace2 PUBLIC cxx_std_17) @@ -27,9 +37,20 @@ include(resources.cmake) SET_TARGET_PROPERTIES(Freespace2 PROPERTIES OUTPUT_NAME "fs2_open_${FSO_BINARY_SUFFIX}") -TARGET_LINK_LIBRARIES(Freespace2 code) -TARGET_LINK_LIBRARIES(Freespace2 platform) -TARGET_LINK_LIBRARIES(Freespace2 compiler) +if (ANDROID) + # one call + target_link_libraries(Freespace2 + PRIVATE + code + platform + compiler + ${ANDROID_LIBS} + ) +else() + TARGET_LINK_LIBRARIES(Freespace2 code) + TARGET_LINK_LIBRARIES(Freespace2 platform) + TARGET_LINK_LIBRARIES(Freespace2 compiler) +endif() if(FSO_BUILD_WITH_OPENGL_DEBUG) add_definitions(-DFS_OPENGL_DEBUG) @@ -46,6 +67,8 @@ IF(USE_STLPORT) INCLUDE_DIRECTORIES(BEFORE SYSTEM ${STLPORT_INCLUDE_DIRS}) ENDIF(USE_STLPORT) +# Do not install when building android +if (NOT ANDROID) # Add install script to copy the executable to the FreeSpace path if possible and wanted INSTALL( TARGETS Freespace2 @@ -59,6 +82,7 @@ if (FSO_INSTALL_DEBUG_FILES) OPTIONAL) endif() endif() +endif(NOT ANDROID) if(FSO_BUILD_APPIMAGE) configure_file("${CMAKE_CURRENT_LIST_DIR}/cmake/AppRun.in" "${CMAKE_CURRENT_BINARY_DIR}/AppRun.gen" @ONLY) @@ -84,6 +108,8 @@ enable_clang_tidy(Freespace2) INCLUDE(util) COPY_FILES_TO_TARGET(Freespace2) +# Do not launch when building android +if (NOT ANDROID) include(CreateLaunchers) create_target_launcher(Freespace2 WORKING_DIRECTORY ${FSO_FREESPACE_PATH} @@ -96,6 +122,7 @@ add_custom_target(launch_fso WORKING_DIRECTORY "${FSO_FREESPACE_PATH}" COMMENT "Launching FreeSpace" VERBATIM) +endif() # Ensure that Xcode generates debug symbols on macOS for Debug builds set_target_properties(Freespace2 PROPERTIES XCODE_ATTRIBUTE_GCC_GENERATE_DEBUGGING_SYMBOLS[variant=Debug] "YES") diff --git a/freespace2/SDLGraphicsOperations.cpp b/freespace2/SDLGraphicsOperations.cpp index 4c5a45c2b7..2f1460760a 100644 --- a/freespace2/SDLGraphicsOperations.cpp +++ b/freespace2/SDLGraphicsOperations.cpp @@ -232,6 +232,12 @@ std::unique_ptr SDLGraphicsOperations::createViewport(const os::Vi windowflags |= SDL_WINDOW_MOUSE_GRABBED; } + #ifdef __ANDROID__ + SDL_SetHint(SDL_HINT_ANDROID_TRAP_BACK_BUTTON, "1"); + SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); + os_android_touch_overlay_init(); + #endif + SDL_Rect bounds; if ( !SDL_GetDisplayBounds(props.display, &bounds) ) { mprintf(("Failed to get display bounds: %s\n", SDL_GetError())); diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index 5cfd17526b..fd5a38e90d 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -8301,3 +8301,10 @@ int main(int argc, char *argv[]) return result; } + +#ifdef __ANDROID__ +extern "C" int android_main(int argc, char* argv[]) +{ + return main(argc, argv); +} +#endif \ No newline at end of file diff --git a/lib/FFmpeg.cmake b/lib/FFmpeg.cmake index 740ab4c1c1..cfbbb367d9 100644 --- a/lib/FFmpeg.cmake +++ b/lib/FFmpeg.cmake @@ -48,7 +48,7 @@ IF(PLATFORM_WINDOWS) target_link_libraries(ffmpeg INTERFACE swscale) target_link_libraries(ffmpeg INTERFACE swresample) ELSE(WIN32) - if (PLATFORM_MAC) + if (PLATFORM_MAC OR ANDROID) option(FFMPEG_USE_PRECOMPILED "Use precompiled version of FFmpeg. If disabled the system libraries will be used." ON) else() option(FFMPEG_USE_PRECOMPILED "Use precompiled version of FFmpeg. If disabled the system libraries will be used." OFF) @@ -97,8 +97,8 @@ ELSE(WIN32) # Use our libraries find_library(${name}_LOCATION ${name} PATHS "${FFMPEG_PATH}/lib" - NO_DEFAULT_PATH) - + NO_DEFAULT_PATH + NO_CMAKE_FIND_ROOT_PATH) file(GLOB ${name}_LIBS "${FFMPEG_PATH}/lib/lib${name}*") get_filename_component(FULL_LIB_PATH "${${name}_LOCATION}" REALPATH) diff --git a/lib/OpenAL.cmake b/lib/OpenAL.cmake index 67adfd8c3a..ac845fc441 100644 --- a/lib/OpenAL.cmake +++ b/lib/OpenAL.cmake @@ -58,7 +58,7 @@ ELSE(WIN32) message(STATUS "Using pre-built OpenAL library.") unset(OpenAL_LOCATION CACHE) - find_library(OpenAL_LOCATION openal PATHS "${OpenAL_ROOT_DIR}/lib" NO_DEFAULT_PATH) + find_library(OpenAL_LOCATION openal PATHS "${OpenAL_ROOT_DIR}/lib" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) get_filename_component(FULL_LIB_PATH "${OpenAL_LOCATION}" REALPATH) ADD_IMPORTED_LIB(openal "${OpenAL_ROOT_DIR}/include" "${FULL_LIB_PATH}") diff --git a/lib/SDL3.cmake b/lib/SDL3.cmake index 51e6375ab6..9fb0fa3e29 100644 --- a/lib/SDL3.cmake +++ b/lib/SDL3.cmake @@ -66,7 +66,7 @@ else() message(STATUS "Using pre-built SDL3 library.") unset(SDL3_LOCATION CACHE) - find_library(SDL3_LOCATION NAMES SDL3 SDL3-3.0 PATHS "${SDL3_ROOT_DIR}/lib" NO_DEFAULT_PATH) + find_library(SDL3_LOCATION NAMES SDL3 SDL3-3.0 PATHS "${SDL3_ROOT_DIR}/lib" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) get_filename_component(FULL_LIB_PATH "${SDL3_LOCATION}" REALPATH) ADD_IMPORTED_LIB(sdl3 "${SDL3_ROOT_DIR}/include" "${FULL_LIB_PATH}") diff --git a/lib/freetype.cmake b/lib/freetype.cmake index cda855d3f5..a64e5f85c8 100644 --- a/lib/freetype.cmake +++ b/lib/freetype.cmake @@ -1,5 +1,5 @@ -if(PLATFORM_WINDOWS OR PLATFORM_MAC OR CMAKE_CROSSCOMPILING) +if(PLATFORM_WINDOWS OR PLATFORM_MAC OR CMAKE_CROSSCOMPILING OR ANDROID) add_library(freetype INTERFACE) # We use prebuilt binaries for windows and mac @@ -10,7 +10,7 @@ if(PLATFORM_WINDOWS OR PLATFORM_MAC OR CMAKE_CROSSCOMPILING) set(SEARCH_PATH "${FREETYPE_ROOT_DIR}/lib") - if(PLATFORM_WINDOWS OR CMAKE_CROSSCOMPILING) + if((PLATFORM_WINDOWS OR CMAKE_CROSSCOMPILING) AND NOT ANDROID) set(LIBNAME "freetype*.dll") else() set(LIBNAME "libfreetype*") @@ -21,7 +21,8 @@ if(PLATFORM_WINDOWS OR PLATFORM_MAC OR CMAKE_CROSSCOMPILING) find_library(freetype_LOCATION NAMES freetype freetype281 PATHS "${SEARCH_PATH}" - NO_DEFAULT_PATH) + NO_DEFAULT_PATH + NO_CMAKE_FIND_ROOT_PATH) if (${freetype_LOCATION} STREQUAL "freetype_LOCATION-NOTFOUND" AND CMAKE_CROSSCOMPILING) #Just try the default here and see if we can make it work. diff --git a/lib/prebuilt.cmake b/lib/prebuilt.cmake index 7be58f7122..0f53b82268 100644 --- a/lib/prebuilt.cmake +++ b/lib/prebuilt.cmake @@ -3,6 +3,7 @@ # without setting Qt6 up project wide set(PREBUILT_VERSION_NAME "b561f6e8" CACHE INTERNAL "") +set(ANDROID_PREBUILT_VERSION_NAME "android-prebuilts-v1" CACHE INTERNAL "") set(FSO_PREBUILT_OVERRIDE "" CACHE PATH "Path to the prebuilt binaries, if empty the binaries will be downloaded.") set(PREBUILT_LIB_DIR "${CMAKE_CURRENT_BINARY_DIR}/prebuilt" CACHE INTERNAL "") @@ -14,15 +15,9 @@ function(get_prebuilt_path OUT_VAR) return() endif() - if (IS_DIRECTORY "${PREBUILT_LIB_DIR}") - if ("${DOWNLOADED_PREBUILT_VERSION}" STREQUAL "${PREBUILT_VERSION_NAME}") - # Libraries already downloaded and up-to-date - set(${OUT_VAR} "${PREBUILT_LIB_DIR}" PARENT_SCOPE) - return() - endif() - endif() - set(PREBUILT_PATH) + set(PREBUILT_CACHE_KEY "${PREBUILT_VERSION_NAME}") + set(DOWNLOAD_HASH_ARGS) set(TAG_NAME "bin-${PREBUILT_VERSION_NAME}") if(PLATFORM_WINDOWS) if (IS_ARM64) @@ -34,6 +29,22 @@ function(get_prebuilt_path OUT_VAR) endif() elseif(PLATFORM_MAC) set(FILENAME "bin-mac.tar.gz") + elseif(ANDROID) + if(ANDROID_ABI STREQUAL "arm64-v8a") + set(FILENAME "bin-android-arm64.tar.gz") + set(PREBUILT_SHA256 "d56464fba097c16c6034ae483215045353078c0a339bb1566f0f7e20d063ca52") + elseif(ANDROID_ABI STREQUAL "x86_64") + set(FILENAME "bin-android-x64.tar.gz") + set(PREBUILT_SHA256 "ab030d21200fb3ef799bb08d7a27ae973274a239dd244f51d68304e90a4021d5") + elseif(ANDROID_ABI STREQUAL "armeabi-v7a") + set(FILENAME "bin-android-arm32.tar.gz") + set(PREBUILT_SHA256 "8c040685cb4e91cf1e95d106200aa8d852f4f32859e3d1fe6d8e995168b122b6") + elseif(ANDROID_ABI STREQUAL "x86") + set(FILENAME "bin-android-x86.tar.gz") + set(PREBUILT_SHA256 "020bf41dc8c1d7414c12c050b2e3abc0a701795073ed48b18706b2a89c30e473") + else() + message(FATAL_ERROR "Unsupported Android ABI for prebuilt libraries: ${ANDROID_ABI}") + endif() else() # Use Linux binaries... if (IS_ARM64) @@ -42,7 +53,23 @@ function(get_prebuilt_path OUT_VAR) set(FILENAME "bin-linux.tar.gz") endif() endif() - set(DOWNLOAD_URL "https://github.com/scp-fs2open/scp-prebuilt/releases/download/${TAG_NAME}/${FILENAME}") + if(ANDROID) # TODO: REMOVE THIS ONCE ANDROID PREBUILTS ARE IN THE SCP-PREBUILT REPO + set(TAG_NAME "${ANDROID_PREBUILT_VERSION_NAME}") + set(DOWNLOAD_URL "https://github.com/Shivansps/Fso-Android-Prebuilts/releases/download/${TAG_NAME}/${FILENAME}") + set(DOWNLOAD_HASH_ARGS EXPECTED_HASH "SHA256=${PREBUILT_SHA256}") + # Re-download when the Android release, ABI, or expected archive changes. + set(PREBUILT_CACHE_KEY "${TAG_NAME}/${ANDROID_ABI}/${PREBUILT_SHA256}") + else() + set(DOWNLOAD_URL "https://github.com/scp-fs2open/scp-prebuilt/releases/download/${TAG_NAME}/${FILENAME}") + endif() + + if (IS_DIRECTORY "${PREBUILT_LIB_DIR}") + if ("${DOWNLOADED_PREBUILT_VERSION}" STREQUAL "${PREBUILT_CACHE_KEY}") + # Libraries already downloaded and up-to-date + set(${OUT_VAR} "${PREBUILT_LIB_DIR}" PARENT_SCOPE) + return() + endif() + endif() set(DOWNLOAD_FILE "${CURRENT_ROOT}/${FILENAME}") set(MAX_RETRIES 5) @@ -53,7 +80,8 @@ function(get_prebuilt_path OUT_VAR) endif() message(STATUS "Downloading prebuilt libraries from \"${DOWNLOAD_URL}\" (try ${i}/${MAX_RETRIES})") - file(DOWNLOAD "${DOWNLOAD_URL}" "${DOWNLOAD_FILE}" SHOW_PROGRESS TLS_VERIFY ON STATUS DOWNLOAD_STATUS_LIST) + file(DOWNLOAD "${DOWNLOAD_URL}" "${DOWNLOAD_FILE}" SHOW_PROGRESS TLS_VERIFY ON + ${DOWNLOAD_HASH_ARGS} STATUS DOWNLOAD_STATUS_LIST) list(GET DOWNLOAD_STATUS_LIST 0 DOWNLOAD_STATUS) list(GET DOWNLOAD_STATUS_LIST 1 DOWNLOAD_ERROR) @@ -95,6 +123,6 @@ function(get_prebuilt_path OUT_VAR) file(REMOVE "${DOWNLOAD_FILE}") # We are done now. Set the cache variables and return the result - set(DOWNLOADED_PREBUILT_VERSION "${PREBUILT_VERSION_NAME}" CACHE INTERNAL "") + set(DOWNLOADED_PREBUILT_VERSION "${PREBUILT_CACHE_KEY}" CACHE INTERNAL "") set(${OUT_VAR} "${PREBUILT_LIB_DIR}" PARENT_SCOPE) endfunction(get_prebuilt_path) diff --git a/lib/vulkan.cmake b/lib/vulkan.cmake index 395ec08452..c788118a38 100644 --- a/lib/vulkan.cmake +++ b/lib/vulkan.cmake @@ -95,7 +95,7 @@ option(VULKAN_USE_PRECOMPILED "Force use of precompiled versions of Vulkan-Loade set(USING_PREBUILT_VULKAN ${VULKAN_USE_PRECOMPILED}) -if(PLATFORM_WINDOWS OR PLATFORM_MAC) +if(PLATFORM_WINDOWS OR PLATFORM_MAC OR ANDROID) set(USING_PREBUILT_VULKAN TRUE) elseif(PLATFORM_LINUX AND FSO_BUILD_APPIMAGE) set(USING_PREBUILT_VULKAN TRUE) @@ -141,7 +141,7 @@ if(USING_PREBUILT_VULKAN) find_library(MOLTENVK_LIBRARY MoltenVK PATHS "${PREBUILT_PATH}" NO_DEFAULT_PATH) add_target_copy_files("${MOLTENVK_LIBRARY}") - else() + elseif(NOT ANDROID) # Android ships its own Vulkan loader (system libvulkan.so) message(STATUS "Using pre-built Vulkan-Loader library.") if(PLATFORM_WINDOWS)