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
# 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
# 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
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
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
# 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)
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