gamnit: rewrite android support
authorAlexis Laferrière <alexis.laf@xymus.net>
Tue, 3 Oct 2017 20:15:52 +0000 (16:15 -0400)
committerAlexis Laferrière <alexis.laf@xymus.net>
Wed, 17 Jan 2018 17:44:33 +0000 (12:44 -0500)
Signed-off-by: Alexis Laferrière <alexis.laf@xymus.net>

lib/android/native_app_glue.nit
lib/gamnit/display_android.nit
lib/gamnit/egl.nit
lib/gamnit/gamnit_android.nit

index 1c21dec..6b9c0b5 100644 (file)
@@ -52,13 +52,27 @@ in "C header" `{
 in "C body" `{
        struct android_app* native_app_glue_data;
 
+       // Was `android_main` called?
+       int android_main_launched = 0;
+
        // Entry point called by the native_app_glue_framework framework
        // We relay the call to the Nit application.
        void android_main(struct android_app* app) {
                native_app_glue_data = app;
 
-               int main(int argc, char ** argv);
-               main(0, NULL);
+               if (android_main_launched) {
+                       // Second call to `android_main`, may happen if `exit 0` was not
+                       // called previously to force unloading the Nit app state.
+                       // This happens sometimes when the `destroy` lifecycle command
+                       // was not correctly received.
+                       // We `exit 0` here hoping the system restarts the app nicely
+                       // without an error popup.
+                       exit(0);
+               } else {
+                       android_main_launched = 1;
+                       int main(int argc, char ** argv);
+                       main(0, NULL);
+               }
        }
 
        // Main callback on the native_app_glue framework
@@ -360,7 +374,7 @@ extern class NativeAppGlue `{ struct android_app* `}
 
        # This is non-zero when the application's NativeActivity is being
        # destroyed and waiting for the app thread to complete.
-       fun detroy_request: Bool `{ return self->destroyRequested; `}
+       fun destroy_requested: Bool `{ return self->destroyRequested; `}
 end
 
 # Android NDK's struture holding configurations of the native app
index 6b069d3..14a3762 100644 (file)
 
 # Gamnit display implementation for Android
 #
-# Generated APK files require OpenGL ES 2.0.
+# Gamnit apps on Android require OpenGL ES 3.0 because, even if it uses only
+# the OpenGL ES 2.0 API, the default shaders have more than 8 vertex attributes.
+# OpenGL ES 3.0 ensures at least 8 vertex attributes, while 2.0 ensures only 4.
 #
-# This modules uses `android::native_app_glue` and the Android NDK.
+# This module relies on `android::native_app_glue` and the Android NDK.
 module display_android is
-       android_manifest """<uses-feature android:glEsVersion="0x00020000"/>"""
+       android_manifest """<uses-feature android:glEsVersion="0x00030000" android:required="true" />"""
 end
 
 import ::android::game
@@ -40,6 +42,7 @@ redef class GamnitDisplay
                select_egl_config(red_bits, green_bits, blue_bits, 0, 8, 0, 0)
 
                var format = egl_config.attribs(egl_display).native_visual_id
+               assert not native_window.address_is_null
                native_window.set_buffers_geometry(0, 0, format)
 
                setup_egl_context native_window
index b0f756f..f8c7a1c 100644 (file)
@@ -96,6 +96,47 @@ redef class GamnitDisplay
                assert egl_bind_opengl_es_api else print "EGL bind API failed: {egl_display.error}"
        end
 
+       # Check if the current configuration of `native_window` is still valid
+       #
+       # There is two return values:
+       # * Returns `true` if the Gamnit services should be recreated.
+       # * Sets `native_window_is_invalid` if the system provided window handle is invalid.
+       #   We should wait until we are provided a valid window handle.
+       fun check_egl_context(native_window: Pointer): Bool
+       do
+               native_window_is_invalid = false
+
+               if not egl_context.is_ok then
+                       # Needs recreating
+                       egl_context = egl_display.create_context(egl_config)
+                       assert egl_context.is_ok else print "Creating EGL context failed: {egl_display.error}"
+               end
+
+               var success = egl_display.make_current(window_surface, window_surface, egl_context)
+               if not success then
+                       var error = egl_display.error
+                       print "check_egl_context make_current: {error}"
+
+
+                       if error.is_bad_native_window then
+                               # native_window is invalid
+                               native_window_is_invalid = true
+                               return true
+
+                       else if not error.is_success then
+                               # The context is now invalid, rebuild it
+                               setup_egl_context native_window
+                               return true
+                       end
+               end
+               return false
+       end
+
+       # Return value from `check_egl_context`, the current native window is invalid
+       #
+       # We should wait until we are provided a valid window handle.
+       var native_window_is_invalid = false
+
        redef fun width do return window_surface.attribs(egl_display).width
 
        redef fun height do return window_surface.attribs(egl_display).height
index ff66e0b..83bb564 100644 (file)
 # limitations under the License.
 
 # Support services for Gamnit on Android
-module gamnit_android
+module gamnit_android is
+       android_api_min 15
+       android_api_target 15
+       android_manifest_activity """android:theme="@android:style/Theme.NoTitleBar.Fullscreen""""
+       android_manifest_activity """android:configChanges="orientation|screenSize|keyboard|keyboardHidden""""
+end
 
 import android
 
 intrude import gamnit
 intrude import android::input_events
+import egl
+
+private import realtime
+
+# Print Android lifecycle events to the log?
+fun print_lifecycle_events: Bool do return true
 
 redef class App
+
+       # ---
+       # User inputs
+
        redef fun feed_events do app.poll_looper 0
 
        redef fun native_input_key(event) do return accept_event(event)
 
        redef fun native_input_motion(event)
        do
+               if not scene_created then return false
+
                var ie = new AndroidMotionEvent(event)
                var handled = accept_event(ie)
 
@@ -34,4 +51,257 @@ redef class App
 
                return handled
        end
+
+       # ---
+       # Handle OS lifecycle and set current state flag
+
+       # State between `init_window` and `term_window`
+       private var window_created = false
+
+       # State between `gained_focus` and `lost_focus`
+       private var focused = false
+
+       # State between `resume` and `pause`
+       private var resumed = false
+
+       # Stage after `destroy`
+       private var destroyed = false
+
+       redef fun init_window
+       do
+               if print_lifecycle_events then print "+ init_window"
+               window_created = true
+               set_active
+               super
+       end
+
+       redef fun term_window
+       do
+               if print_lifecycle_events then print "+ term_window"
+               window_created = false
+               set_inactive
+               super
+       end
+
+       redef fun resume
+       do
+               if print_lifecycle_events then print "+ resume"
+               resumed = true
+               set_active
+               super
+       end
+
+       redef fun pause
+       do
+               if print_lifecycle_events then print "+ pause"
+               resumed = false
+               set_inactive
+               super
+       end
+
+       redef fun gained_focus
+       do
+               if print_lifecycle_events then print "+ gained_focus"
+               focused = true
+               set_active
+               super
+       end
+
+       redef fun lost_focus
+       do
+               if print_lifecycle_events then print "+ lost_focus"
+               focused = false
+               super
+       end
+
+       redef fun destroy
+       do
+               if print_lifecycle_events then print "+ destroy"
+               destroyed = true
+               super
+       end
+
+       redef fun start
+       do
+               if print_lifecycle_events then print "+ start"
+               super
+       end
+
+       redef fun stop
+       do
+               if print_lifecycle_events then print "+ stop"
+               set_inactive
+               super
+       end
+
+       redef fun config_changed
+       do
+               if print_lifecycle_events then print "+ config_changed"
+               super
+       end
+
+       redef fun window_resized
+       do
+               if print_lifecycle_events then print "+ window_resized"
+               super
+       end
+
+       redef fun content_rect_changed
+       do
+               if print_lifecycle_events then print "+ content_rect_changed"
+               super
+       end
+
+       # ---
+       # Update gamnit app
+
+       # The app is fully visible and focused
+       private var active = false
+
+       # The scene was set up
+       private var scene_created = false
+
+       private fun set_active
+       do
+               assert not destroyed
+               if window_created and resumed and focused and not active then
+                       var display = display
+                       if display == null then
+                               # Initial create
+                               create_display
+                               create_gamnit
+                               display = self.display
+                       else
+                               # Try to reuse the EGL context
+                               var native_window = app.native_app_glue.window
+                               assert not native_window.address_is_null
+                               var needs_recreate = display.check_egl_context(native_window)
+                               if needs_recreate then
+
+                                       # Skip frame
+                                       if display.native_window_is_invalid then
+                                               print_error "the native window is invalid, skip frame"
+                                               return
+                                       end
+
+                                       # The context was lost, reload everything
+                                       create_gamnit
+                                       recreate_gamnit
+                               end
+                       end
+
+                       # Update screen dimensions
+                       assert display != null
+                       display.update_size
+                       app.on_resize display
+
+                       if not scene_created then
+                               # Initial launch
+                               if debug_gamnit then print "set_active: create"
+                               create_scene
+                               scene_created = true
+                               on_restore_state
+                       else
+                               # Next to first launch, reload
+                               if debug_gamnit then print "set_active: recreate"
+                       end
+
+                       active = true
+               end
+       end
+
+       private fun set_inactive
+       do
+               active = false
+       end
+
+       # ---
+       # Implement gamnit entry points
+
+       redef fun recreate_gamnit
+       do
+               super
+
+               # Reload all textures
+               if debug_gamnit then print "recreate_gamnit: reloading {all_root_textures.length} textures"
+               for texture in all_root_textures do
+                       if debug_gamnit then print "recreate_gamnit: loading {texture}"
+                       texture.load true
+                       var gamnit_error = texture.error
+                       if gamnit_error != null then print_error gamnit_error
+               end
+       end
+
+       redef fun run
+       do
+               if debug_gamnit then print "run: start"
+               scene_created = false
+
+               while not destroyed do
+                       if not active then
+                               if debug_gamnit then print "run: wait"
+                               app.poll_looper_pause -1
+
+                       else
+                               if debug_gamnit then print "run: frame"
+
+                               var native_window = app.native_app_glue.window
+                               assert not native_window.address_is_null
+
+                               var display = display
+                               assert display != null
+
+                               var needs_recreate = display.check_egl_context(native_window)
+                               if needs_recreate then
+                                       if display.native_window_is_invalid then
+                                               # This should be rare and may cause more issues, log it
+                                               print "The native window is invalid, skip frame"
+                                               set_inactive
+                                               continue
+                                       end
+
+                                       # The context was lost, reload everything
+                                       create_gamnit
+                                       recreate_gamnit
+                               end
+
+                               assert scene_created
+                               frame_full
+                       end
+               end
+
+               if debug_gamnit then print "run: exit"
+               exit 0
+       end
+end
+
+redef class GamnitDisplay
+
+       redef var width = 1080
+       redef var height = 720
+
+       # Update `width` and `height`
+       private fun update_size
+       do
+               var context = app.native_activity
+               self.width = context.window_width
+               self.height = context.window_height
+       end
+end
+
+redef class NativeActivity
+
+       private fun window_height: Int in "Java" `{
+               android.view.View view = self.getWindow().getDecorView();
+               return view.getBottom() - view.getTop();
+       `}
+
+       private fun window_width: Int in "Java" `{
+               android.view.View view = self.getWindow().getDecorView();
+               return view.getRight() - view.getLeft();
+       `}
+
+       private fun orientation: Int in "Java" `{
+               return self.getResources().getConfiguration().orientation;
+       `}
 end