diff --git a/.clang-format b/.clang-format index f9aa6536d..e4310ac3c 100644 --- a/.clang-format +++ b/.clang-format @@ -6,3 +6,8 @@ SortIncludes: true # Regroup causes unnecessary noise due to clang-format bug. IncludeBlocks: Preserve + +--- +Language: Java +DisableFormat: true +SortIncludes: false diff --git a/android/android_studio_project/app/src/main/AndroidManifest.xml b/android/android_studio_project/app/src/main/AndroidManifest.xml index c5c7c703f..8f6d53cb0 100644 --- a/android/android_studio_project/app/src/main/AndroidManifest.xml +++ b/android/android_studio_project/app/src/main/AndroidManifest.xml @@ -2,12 +2,23 @@ - - - + + + + + - - + + - + + + \ No newline at end of file diff --git a/android/android_studio_project/app/src/main/java/jp/xenia/emulator/DemoActivity.java b/android/android_studio_project/app/src/main/java/jp/xenia/emulator/DemoActivity.java deleted file mode 100644 index 970bd9b03..000000000 --- a/android/android_studio_project/app/src/main/java/jp/xenia/emulator/DemoActivity.java +++ /dev/null @@ -1,12 +0,0 @@ -package jp.xenia.emulator; - -import android.app.Activity; -import android.os.Bundle; - -public class DemoActivity extends Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_demo); - } -} \ No newline at end of file diff --git a/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowDemoActivity.java b/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowDemoActivity.java new file mode 100644 index 000000000..a0dd36f0e --- /dev/null +++ b/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowDemoActivity.java @@ -0,0 +1,8 @@ +package jp.xenia.emulator; + +public class WindowDemoActivity extends WindowedAppActivity { + @Override + protected String getWindowedAppIdentifier() { + return "xenia_ui_window_vulkan_demo"; + } +} diff --git a/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowedAppActivity.java b/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowedAppActivity.java new file mode 100644 index 000000000..dd89881c3 --- /dev/null +++ b/android/android_studio_project/app/src/main/java/jp/xenia/emulator/WindowedAppActivity.java @@ -0,0 +1,45 @@ +package jp.xenia.emulator; + +import android.app.Activity; +import android.content.res.AssetManager; +import android.os.Bundle; +import android.util.Log; + +public abstract class WindowedAppActivity extends Activity { + private static final String TAG = "WindowedAppActivity"; + + static { + // TODO(Triang3l): Move all demos to libxenia.so. + System.loadLibrary("xenia-ui-window-vulkan-demo"); + } + + private long mAppContext; + + private native long initializeWindowedAppOnCreateNative( + String windowedAppIdentifier, AssetManager assetManager); + + private native void onDestroyNative(long appContext); + + protected abstract String getWindowedAppIdentifier(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAppContext = initializeWindowedAppOnCreateNative(getWindowedAppIdentifier(), getAssets()); + if (mAppContext == 0) { + Log.e(TAG, "Error initializing the windowed app"); + finish(); + return; + } + } + + @Override + protected void onDestroy() { + if (mAppContext != 0) { + onDestroyNative(mAppContext); + } + mAppContext = 0; + super.onDestroy(); + } +} diff --git a/android/android_studio_project/app/src/main/res/layout/activity_demo.xml b/android/android_studio_project/app/src/main/res/layout/activity_window_demo.xml similarity index 76% rename from android/android_studio_project/app/src/main/res/layout/activity_demo.xml rename to android/android_studio_project/app/src/main/res/layout/activity_window_demo.xml index ed5456938..79f49f81a 100644 --- a/android/android_studio_project/app/src/main/res/layout/activity_demo.xml +++ b/android/android_studio_project/app/src/main/res/layout/activity_window_demo.xml @@ -3,6 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="jp.xenia.emulator.DemoActivity"> + tools:context="jp.xenia.emulator.WindowDemoActivity"> \ No newline at end of file diff --git a/src/xenia/ui/windowed_app.cc b/src/xenia/ui/windowed_app.cc new file mode 100644 index 000000000..8e19674ec --- /dev/null +++ b/src/xenia/ui/windowed_app.cc @@ -0,0 +1,25 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2021 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/ui/windowed_app.h" + +#include +#include + +namespace xe { +namespace ui { + +#if XE_UI_WINDOWED_APPS_IN_LIBRARY +// A zero-initialized pointer to remove dependence on the initialization order +// of the map relatively to the app creator proxies. +std::unordered_map* WindowedApp::creators_; +#endif // XE_UI_WINDOWED_APPS_IN_LIBRARY + +} // namespace ui +} // namespace xe diff --git a/src/xenia/ui/windowed_app.h b/src/xenia/ui/windowed_app.h index b521a759e..89d4eff1f 100644 --- a/src/xenia/ui/windowed_app.h +++ b/src/xenia/ui/windowed_app.h @@ -13,15 +13,17 @@ #include #include #include +#include +#include #include +#include "xenia/base/assert.h" #include "xenia/base/platform.h" #include "xenia/ui/windowed_app_context.h" #if XE_PLATFORM_ANDROID -#include - -#include "xenia/ui/windowed_app_context_android.h" +// Multiple apps in a single library instead of separate executables. +#define XE_UI_WINDOWED_APPS_IN_LIBRARY 1 #endif namespace xe { @@ -36,6 +38,9 @@ class WindowedApp { // initialization of platform-specific parts, should preferably be as simple // as possible). + using Creator = std::unique_ptr (*)( + xe::ui::WindowedAppContext& app_context); + WindowedApp(const WindowedApp& app) = delete; WindowedApp& operator=(const WindowedApp& app) = delete; virtual ~WindowedApp() = default; @@ -101,27 +106,67 @@ class WindowedApp { std::string name_; std::string positional_options_usage_; std::vector positional_options_; + +#if XE_UI_WINDOWED_APPS_IN_LIBRARY + public: + class CreatorRegistration { + public: + CreatorRegistration(const std::string_view identifier, Creator creator) { + if (!creators_) { + // Will be deleted by the last creator registration's destructor, no + // need for a library destructor. + creators_ = new std::unordered_map; + } + iterator_inserted_ = creators_->emplace(identifier, creator); + assert_true(iterator_inserted_.second); + } + + ~CreatorRegistration() { + if (iterator_inserted_.second) { + creators_->erase(iterator_inserted_.first); + if (creators_->empty()) { + delete creators_; + } + } + } + + private: + std::pair::iterator, bool> + iterator_inserted_; + }; + + static Creator GetCreator(const std::string& identifier) { + if (!creators_) { + return nullptr; + } + auto it = creators_->find(identifier); + return it != creators_->end() ? it->second : nullptr; + } + + private: + static std::unordered_map* creators_; +#endif // XE_UI_WINDOWED_APPS_IN_LIBRARY }; -#if XE_PLATFORM_ANDROID -// Multiple apps in a single library. ANativeActivity_onCreate chosen via -// android.app.func_name of the NativeActivity of each app. -#define XE_DEFINE_WINDOWED_APP(export_name, creator) \ - __attribute__((visibility("default"))) extern "C" void export_name( \ - ANativeActivity* activity, void* saved_state, size_t saved_state_size) { \ - xe::ui::AndroidWindowedAppContext::StartAppOnActivityCreate( \ - activity, saved_state, saved_state_size, creator); \ +#if XE_UI_WINDOWED_APPS_IN_LIBRARY +// Multiple apps in a single library. +#define XE_DEFINE_WINDOWED_APP(identifier, creator) \ + namespace xe { \ + namespace ui { \ + namespace windowed_app_creator_registrations { \ + xe::ui::WindowedApp::CreatorRegistration identifier(#identifier, creator); \ + } \ + } \ } #else // Separate executables for each app. std::unique_ptr (*GetWindowedAppCreator())( WindowedAppContext& app_context); -#define XE_DEFINE_WINDOWED_APP(export_name, creator) \ - std::unique_ptr (*xe::ui::GetWindowedAppCreator())( \ - xe::ui::WindowedAppContext & app_context) { \ - return creator; \ +#define XE_DEFINE_WINDOWED_APP(identifier, creator) \ + xe::ui::WindowedApp::Creator xe::ui::GetWindowedAppCreator() { \ + return creator; \ } -#endif +#endif // XE_UI_WINDOWED_APPS_IN_LIBRARY } // namespace ui } // namespace xe diff --git a/src/xenia/ui/windowed_app_context_android.cc b/src/xenia/ui/windowed_app_context_android.cc index fcab4f5fd..5af4efad6 100644 --- a/src/xenia/ui/windowed_app_context_android.cc +++ b/src/xenia/ui/windowed_app_context_android.cc @@ -9,10 +9,12 @@ #include "xenia/ui/windowed_app_context_android.h" +#include #include +#include #include -#include #include +#include #include #include #include @@ -25,30 +27,6 @@ namespace xe { namespace ui { -void AndroidWindowedAppContext::StartAppOnActivityCreate( - ANativeActivity* activity, [[maybe_unused]] void* saved_state, - [[maybe_unused]] size_t saved_state_size, - std::unique_ptr (*app_creator)( - WindowedAppContext& app_context)) { - // TODO(Triang3l): Pass the launch options from the Intent or the saved - // instance state. - AndroidWindowedAppContext* app_context = new AndroidWindowedAppContext; - if (!app_context->Initialize(activity)) { - delete app_context; - ANativeActivity_finish(activity); - return; - } - // The pointer is now held by the Activity as its ANativeActivity::instance, - // until the destruction. - if (!app_context->InitializeApp(app_creator)) { - // InitializeApp might have sent commands to the UI thread looper callback - // pipe, perform deferred destruction. - app_context->RequestDestruction(); - ANativeActivity_finish(activity); - return; - } -} - void AndroidWindowedAppContext::NotifyUILoopOfPendingFunctions() { // Don't check ui_thread_looper_callback_registered_, as it's owned // exclusively by the UI thread, while this may be called by any, and in case @@ -69,22 +47,145 @@ void AndroidWindowedAppContext::NotifyUILoopOfPendingFunctions() { void AndroidWindowedAppContext::PlatformQuitFromUIThread() { // All the shutdown will be done in onDestroy of the activity. - ANativeActivity_finish(activity_); + if (activity_ && activity_method_finish_) { + ui_thread_jni_env_->CallVoidMethod(activity_, activity_method_finish_); + } +} + +AndroidWindowedAppContext* +AndroidWindowedAppContext::JniActivityInitializeWindowedAppOnCreate( + JNIEnv* jni_env, jobject activity, jstring windowed_app_identifier, + jobject asset_manager) { + WindowedApp::Creator app_creator; + { + const char* windowed_app_identifier_c_str = + jni_env->GetStringUTFChars(windowed_app_identifier, nullptr); + if (!windowed_app_identifier_c_str) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to get the UTF-8 string for the windowed app identifier"); + return nullptr; + } + app_creator = WindowedApp::GetCreator(windowed_app_identifier_c_str); + if (!app_creator) { + __android_log_print(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to get the creator for the windowed app %s", + windowed_app_identifier_c_str); + jni_env->ReleaseStringUTFChars(windowed_app_identifier, + windowed_app_identifier_c_str); + return nullptr; + } + jni_env->ReleaseStringUTFChars(windowed_app_identifier, + windowed_app_identifier_c_str); + } + + AndroidWindowedAppContext* app_context = new AndroidWindowedAppContext; + if (!app_context->Initialize(jni_env, activity, asset_manager)) { + delete app_context; + return nullptr; + } + + if (!app_context->InitializeApp(app_creator)) { + // InitializeApp might have sent commands to the UI thread looper callback + // pipe, perform deferred destruction. + app_context->RequestDestruction(); + return nullptr; + } + + return app_context; +} + +void AndroidWindowedAppContext::JniActivityOnDestroy() { + if (app_) { + app_->InvokeOnDestroy(); + app_.reset(); + } + RequestDestruction(); } AndroidWindowedAppContext::~AndroidWindowedAppContext() { Shutdown(); } -bool AndroidWindowedAppContext::Initialize(ANativeActivity* activity) { - int32_t api_level; - { - AConfiguration* configuration = AConfiguration_new(); - AConfiguration_fromAssetManager(configuration, activity->assetManager); - api_level = AConfiguration_getSdkVersion(configuration); - AConfiguration_delete(configuration); +bool AndroidWindowedAppContext::Initialize(JNIEnv* ui_thread_jni_env, + jobject activity, + jobject asset_manager) { + // Xenia logging is not initialized yet - use __android_log_write or + // __android_log_print until InitializeAndroidAppFromMainThread is done. + + ui_thread_jni_env_ = ui_thread_jni_env; + + // Initialize the asset manager for retrieving the current configuration. + asset_manager_jobject_ = ui_thread_jni_env_->NewGlobalRef(asset_manager); + if (!asset_manager_jobject_) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to create a global reference to the asset manager"); + Shutdown(); + return false; } - xe::InitializeAndroidAppFromMainThread(api_level); + asset_manager_ = + AAssetManager_fromJava(ui_thread_jni_env_, asset_manager_jobject_); + if (!asset_manager_) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to create get the AAssetManager"); + Shutdown(); + return false; + } + + // Get the initial configuration. + configuration_ = AConfiguration_new(); + if (!configuration_) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidWindowedAppContext", + "Failed to create an AConfiguration"); + Shutdown(); + return false; + } + AConfiguration_fromAssetManager(configuration_, asset_manager_); + + // Initialize Xenia globals that may depend on the API level, as well as + // logging. + xe::InitializeAndroidAppFromMainThread( + AConfiguration_getSdkVersion(configuration_)); android_base_initialized_ = true; + // Initialize interfacing with the WindowedAppActivity. + activity_ = ui_thread_jni_env_->NewGlobalRef(activity); + if (!activity_) { + XELOGE( + "AndroidWindowedAppContext: Failed to create a global reference to the " + "activity"); + Shutdown(); + return false; + } + { + jclass activity_class_local_ref = + ui_thread_jni_env_->GetObjectClass(activity); + if (!activity_class_local_ref) { + XELOGE("AndroidWindowedAppContext: Failed to get the activity class"); + Shutdown(); + return false; + } + activity_class_ = reinterpret_cast(ui_thread_jni_env_->NewGlobalRef( + reinterpret_cast(activity_class_local_ref))); + ui_thread_jni_env_->DeleteLocalRef( + reinterpret_cast(activity_class_local_ref)); + } + if (!activity_class_) { + XELOGE( + "AndroidWindowedAppContext: Failed to create a global reference to the " + "activity class"); + Shutdown(); + return false; + } + bool activity_ids_obtained = true; + activity_ids_obtained &= + (activity_method_finish_ = ui_thread_jni_env_->GetMethodID( + activity_class_, "finish", "()V")) != nullptr; + if (!activity_ids_obtained) { + XELOGE("AndroidWindowedAppContext: Failed to get the activity class IDs"); + Shutdown(); + return false; + } + // Initialize sending commands to the UI thread looper callback, for // requesting function calls in the UI thread. ui_thread_looper_ = ALooper_forThread(); @@ -117,10 +218,6 @@ bool AndroidWindowedAppContext::Initialize(ANativeActivity* activity) { } ui_thread_looper_callback_registered_ = true; - activity_ = activity; - activity_->instance = this; - activity_->callbacks->onDestroy = OnActivityDestroy; - return true; } @@ -135,12 +232,6 @@ void AndroidWindowedAppContext::Shutdown() { assert_null(activity_window_); activity_window_ = nullptr; - if (activity_) { - activity_->callbacks->onDestroy = nullptr; - activity_->instance = nullptr; - activity_ = nullptr; - } - if (ui_thread_looper_callback_registered_) { ALooper_removeFd(ui_thread_looper_, ui_thread_looper_callback_pipe_[0]); ui_thread_looper_callback_registered_ = false; @@ -157,10 +248,34 @@ void AndroidWindowedAppContext::Shutdown() { ui_thread_looper_ = nullptr; } + activity_method_finish_ = nullptr; + if (activity_class_) { + ui_thread_jni_env_->DeleteGlobalRef( + reinterpret_cast(activity_class_)); + activity_class_ = nullptr; + } + if (activity_) { + ui_thread_jni_env_->DeleteGlobalRef(activity_); + activity_ = nullptr; + } + if (android_base_initialized_) { xe::ShutdownAndroidAppFromMainThread(); android_base_initialized_ = false; } + + if (configuration_) { + AConfiguration_delete(configuration_); + configuration_ = nullptr; + } + + asset_manager_ = nullptr; + if (asset_manager_jobject_) { + ui_thread_jni_env_->DeleteGlobalRef(asset_manager_jobject_); + asset_manager_jobject_ = nullptr; + } + + ui_thread_jni_env_ = nullptr; } void AndroidWindowedAppContext::RequestDestruction() { @@ -260,15 +375,26 @@ bool AndroidWindowedAppContext::InitializeApp(std::unique_ptr ( return true; } -void AndroidWindowedAppContext::OnActivityDestroy(ANativeActivity* activity) { - auto& app_context = - *static_cast(activity->instance); - if (app_context.app_) { - app_context.app_->InvokeOnDestroy(); - app_context.app_.reset(); - } - app_context.RequestDestruction(); -} - } // namespace ui } // namespace xe + +extern "C" { + +JNIEXPORT jlong JNICALL +Java_jp_xenia_emulator_WindowedAppActivity_initializeWindowedAppOnCreateNative( + JNIEnv* jni_env, jobject activity, jstring windowed_app_identifier, + jobject asset_manager) { + return reinterpret_cast( + xe::ui::AndroidWindowedAppContext :: + JniActivityInitializeWindowedAppOnCreate( + jni_env, activity, windowed_app_identifier, asset_manager)); +} + +JNIEXPORT void JNICALL +Java_jp_xenia_emulator_WindowedAppActivity_onDestroyNative( + JNIEnv* jni_env, jobject activity, jlong app_context_ptr) { + reinterpret_cast(app_context_ptr) + ->JniActivityOnDestroy(); +} + +} // extern "C" diff --git a/src/xenia/ui/windowed_app_context_android.h b/src/xenia/ui/windowed_app_context_android.h index 64e45372d..91cd10427 100644 --- a/src/xenia/ui/windowed_app_context_android.h +++ b/src/xenia/ui/windowed_app_context_android.h @@ -10,8 +10,10 @@ #ifndef XENIA_UI_WINDOWED_APP_CONTEXT_ANDROID_H_ #define XENIA_UI_WINDOWED_APP_CONTEXT_ANDROID_H_ +#include +#include #include -#include +#include #include #include @@ -25,13 +27,6 @@ class WindowedApp; class AndroidWindowedAppContext final : public WindowedAppContext { public: - // For calling from android.app.func_name exports. - static void StartAppOnActivityCreate( - ANativeActivity* activity, void* saved_state, size_t saved_state_size, - std::unique_ptr (*app_creator)( - WindowedAppContext& app_context)); - - ANativeActivity* activity() const { return activity_; } WindowedApp* app() const { return app_.get(); } void NotifyUILoopOfPendingFunctions() override; @@ -45,6 +40,12 @@ class AndroidWindowedAppContext final : public WindowedAppContext { AndroidWindow* GetActivityWindow() const { return activity_window_; } void SetActivityWindow(AndroidWindow* window) { activity_window_ = window; } + // For calling from WindowedAppActivity native methods. + static AndroidWindowedAppContext* JniActivityInitializeWindowedAppOnCreate( + JNIEnv* jni_env, jobject activity, jstring windowed_app_identifier, + jobject asset_manager); + void JniActivityOnDestroy(); + private: enum class UIThreadLooperCallbackCommand : uint8_t { kDestroy, @@ -55,13 +56,14 @@ class AndroidWindowedAppContext final : public WindowedAppContext { // Don't delete this object directly externally if successfully initialized as // the looper may still execute the callback for pending commands after an - // external ANativeActivity_removeFd, and the callback receives a pointer to - // the context - deletion must be deferred and done in the callback itself. + // external ALooper_removeFd, and the callback receives a pointer to the + // context - deletion must be deferred and done in the callback itself. // Defined in the translation unit where WindowedApp is complete because of // std::unique_ptr. ~AndroidWindowedAppContext(); - bool Initialize(ANativeActivity* activity); + bool Initialize(JNIEnv* ui_thread_jni_env, jobject activity, + jobject asset_manager); void Shutdown(); // Call this function instead of deleting the object directly, so if needed, @@ -75,10 +77,29 @@ class AndroidWindowedAppContext final : public WindowedAppContext { bool InitializeApp(std::unique_ptr (*app_creator)( WindowedAppContext& app_context)); - static void OnActivityDestroy(ANativeActivity* activity); + // Useful notes about JNI usage on Android within Xenia: + // - All static libraries defining JNI native functions must be linked to + // shared libraries via LOCAL_WHOLE_STATIC_LIBRARIES. + // - If method or field IDs are cached, a global reference to the class needs + // to be held - it prevents the class from being unloaded by the class + // loaders (in a way that would make the IDs invalid when it's reloaded). + // - GetStringUTFChars (UTF-8) returns null-terminated strings, GetStringChars + // (UTF-16) does not. + JNIEnv* ui_thread_jni_env_ = nullptr; + + // The object reference must be held by the app according to + // AAssetManager_fromJava documentation. + jobject asset_manager_jobject_ = nullptr; + AAssetManager* asset_manager_ = nullptr; + + AConfiguration* configuration_ = nullptr; bool android_base_initialized_ = false; + jobject activity_ = nullptr; + jclass activity_class_ = nullptr; + jmethodID activity_method_finish_ = nullptr; + // May be read by non-UI threads in NotifyUILoopOfPendingFunctions. ALooper* ui_thread_looper_ = nullptr; // [1] (the write file descriptor) may be referenced as read-only by non-UI @@ -86,11 +107,6 @@ class AndroidWindowedAppContext final : public WindowedAppContext { std::array ui_thread_looper_callback_pipe_{-1, -1}; bool ui_thread_looper_callback_registered_ = false; - // TODO(Triang3l): Switch from ANativeActivity to the context itself being the - // object for communication with the Java code when NativeActivity isn't used - // anymore as its functionality is heavily limited. - ANativeActivity* activity_ = nullptr; - AndroidWindow* activity_window_ = nullptr; std::unique_ptr app_;