Android Build (#6992)

* android build support

* Update es_compatibility.h

* Support reporting flags in android

* include pref_path in json v2

* add vulkan headers

* shaderc includes

* remove headers

* force use prebuilt vulkan

* fix vulkan inverted screen

* fix find sdl3 precompiled android

* Update CMakeLists.txt

* replace sdl2 android function names for sdl3 versions

* change sdl includes in osapi

* fix sdl3 includes, again

* rename function to sdl3 name

* remove unused definition

* copy libshaderc_shared.so to output in android

* do not look for vulkan-loader in android

* revert changes to SDL2.cmake

* add comment to vulkan pretransform setup

* cleanup maxsize function

* rename new android functions in osapi

* android trap back button

* Remove calculate_max_resize_max_size

* touch ui opacity control

* change opacity control to int

* fix opacity slider

* Corrected android working folder fallback and set borderless mode as default

* Implement shared helper for JNI calls

* Add version tag and sha256 to android prebuilt libs

* vulkan android check eSuboptimal

* An already requested rebuild also avoids redundant Android surface queries

* add cache bool to cmakelist

* set string ids
This commit is contained in:
Shivansps authored and GitHub committed 2026-11-05 20:56:36 +01:00
1 parent 705d115214
commit 1d0dd687cd
24 files changed
+498 -107

No files matched your search

+11 -2
View File
@@ -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)
+6 -1
View File
@@ -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';
+85 -50
View File
@@ -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<std::pair<uint16_t, uint16_t>>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_t>(json_integer(static_cast<int>(os::ViewportState::Windowed))), "-window");
Cmdline_window = true;
Cmdline_fullscreen_window = false;
}
if ( fullscreen_window_arg.found( ) )
+17
View File
@@ -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!
+4
View File
@@ -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)
{
+2 -2
View File
@@ -6,7 +6,7 @@
#include <direct.h>
#endif
#if !defined __APPLE_CC__ && defined SCP_UNIX
#if !defined __APPLE_CC__ && !defined __ANDROID__ && defined SCP_UNIX
#include<glad/glad_glx.h>
//Required because X defines none and always, which is used later
#undef None
@@ -1394,7 +1394,7 @@ bool gr_opengl_init(std::unique_ptr<os::GraphicsOperations>&& 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!");
}
+2
View File
@@ -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,
+2 -2
View File
@@ -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;
}
+59 -10
View File
@@ -326,17 +326,17 @@ vk::Extent2D windowExtent()
return {static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(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;
+1 -1
View File
@@ -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
+180
View File
@@ -30,6 +30,14 @@
#include <sys/types.h>
#endif
#ifdef __ANDROID__
#include <SDL3/SDL_system.h>
#include <SDL3/SDL.h>
#include <jni.h>
#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 <typename Callback>
static void android_with_static_method(const char* name, const char* signature, Callback&& callback)
{
JNIEnv* env = static_cast<JNIEnv*>(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<jobject>(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<jstring>(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<jfloat>(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<bool>("Input.TouchOverlay",
std::pair<const char*, int>{"Touch Overlay", 1936},
std::pair<const char*, int>{"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<int>("Input.TouchOverlayOpacity",
std::pair<const char*, int>{"Touch Overlay Opacity", 1938},
std::pair<const char*, int>{"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
+17
View File
@@ -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
{
/**
+1 -1
View File
@@ -3,7 +3,7 @@
#define _AL_H
#if defined(__APPLE__)
#if defined(__APPLE__) || defined(__ANDROID__)
#include "al.h"
#include "alc.h"
#else
+13 -13
View File
@@ -4,8 +4,8 @@
#include "utils/unicode.h"
#include "speech.h"
#include <jni.h>
#include "SDL.h"
#include "SDL_system.h"
#include <SDL3/SDL_system.h>
#include <SDL3/SDL.h>
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<std::pair<int, SCP_string>> 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;
+4
View File
@@ -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
+31 -4
View File
@@ -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")
+6
View File
@@ -232,6 +232,12 @@ std::unique_ptr<os::Viewport> 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()));
+7
View File
@@ -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
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+4 -3
View File
@@ -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.
+39 -11
View File
@@ -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)
+2 -2
View File
@@ -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)