fix: don't start the app from main() on android; use the real inset retry
main() must not run real_main on android. The library's DT_INIT points at Odin's _odin_entry_point, which the loader runs the moment NativeActivity dlopens libmain.so -- before the glue calls android_main, so context.user_ptr is nil and backend_init faults writing app.onAppCmd. The proc still has to exist: package main requires it and it is the desktop entry point. The insets retry now uses request_render_at. A plain request_render made before the build is consumed by the frame it is running in, so it schedules nothing and the loop parks with insets unresolved; it only appeared to work because the first frames redraw anyway while the tree settles. Guards the JNI-only paths (show_toast, refresh_insets) behind the android subtarget so the shared real_main is usable on desktop. refresh_insets returns true there rather than false: zero insets are correct where there are no system bars, and returning false would retry every frame forever. Also declares appCategory=game, and enables the engine's frame timings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,10 +13,11 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Dogfooding"
|
android:label="Dogfooding"
|
||||||
|
android:appCategory="game"
|
||||||
android:hasCode="false"
|
android:hasCode="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:debuggable="false">
|
android:debuggable="true">
|
||||||
|
|
||||||
<activity android:name="android.app.NativeActivity"
|
<activity android:name="android.app.NativeActivity"
|
||||||
android:configChanges="orientation|keyboardHidden"
|
android:configChanges="orientation|keyboardHidden"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import "shared:android"
|
import "shared:android"
|
||||||
import "base:runtime"
|
import "base:runtime"
|
||||||
|
import "core:time"
|
||||||
import engine "shared:zephr"
|
import engine "shared:zephr"
|
||||||
import "jni"
|
import "jni"
|
||||||
|
|
||||||
@@ -18,7 +19,18 @@ insets: Edge_Insets
|
|||||||
insets_valid: bool
|
insets_valid: bool
|
||||||
|
|
||||||
main :: proc() {
|
main :: proc() {
|
||||||
real_main()
|
// Android must not start the app from here. This library's DT_INIT points at
|
||||||
|
// Odin's _odin_entry_point, which the dynamic loader runs the moment
|
||||||
|
// NativeActivity dlopens libmain.so -- long before the glue spawns its
|
||||||
|
// thread and calls android_main, and so before context.user_ptr holds the
|
||||||
|
// android_app. Running real_main here reaches backend_init with a nil app
|
||||||
|
// and faults writing app.onAppCmd.
|
||||||
|
//
|
||||||
|
// The proc still has to exist: package main requires it, and it is the
|
||||||
|
// desktop entry point.
|
||||||
|
when ODIN_PLATFORM_SUBTARGET != .Android {
|
||||||
|
real_main()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
real_main :: proc() {
|
real_main :: proc() {
|
||||||
@@ -41,6 +53,11 @@ real_main :: proc() {
|
|||||||
// flat out forever, which on a phone is straight battery burn.
|
// flat out forever, which on a phone is straight battery burn.
|
||||||
engine.set_render_on_change(true)
|
engine.set_render_on_change(true)
|
||||||
|
|
||||||
|
// Rolling work-vs-swap timings to logcat, to tell a slow frame apart from a
|
||||||
|
// frame that is merely waiting on vsync.
|
||||||
|
engine.set_frame_stats(true)
|
||||||
|
|
||||||
|
|
||||||
for !engine.should_quit() {
|
for !engine.should_quit() {
|
||||||
engine.frame_start()
|
engine.frame_start()
|
||||||
defer engine.frame_end()
|
defer engine.frame_end()
|
||||||
@@ -60,11 +77,16 @@ real_main :: proc() {
|
|||||||
if !insets_valid {
|
if !insets_valid {
|
||||||
insets_valid = refresh_insets()
|
insets_valid = refresh_insets()
|
||||||
if !insets_valid {
|
if !insets_valid {
|
||||||
// The decor view attaches asynchronously and produces no event
|
// The decor view attaches asynchronously and emits no event when
|
||||||
// when it does. Under render_on_change the loop would park
|
// it does, so the retry has to drive itself.
|
||||||
// before the retry ever ran, leaving the app bar with no status
|
//
|
||||||
// bar padding.
|
// Deliberately request_render_at, not request_render: a plain
|
||||||
engine.request_render()
|
// request made here -- before the build -- is consumed by the
|
||||||
|
// very frame it is running in, so it schedules nothing and the
|
||||||
|
// loop parks with insets still unresolved. A timed wake is the
|
||||||
|
// one that survives into the next frame. It only works today
|
||||||
|
// because the first frames redraw anyway while the tree settles.
|
||||||
|
engine.request_render_at(engine.get_time() + 16 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,20 +127,33 @@ draw_ui :: proc() {
|
|||||||
// Toast needs a Java looper on this thread. Preparing one is a one-time cost
|
// Toast needs a Java looper on this thread. Preparing one is a one-time cost
|
||||||
// and the native ALooper the engine polls is unaffected -- they're separate.
|
// and the native ALooper the engine polls is unaffected -- they're separate.
|
||||||
show_toast :: proc(msg: string) {
|
show_toast :: proc(msg: string) {
|
||||||
if l, ok := looper_my_looper(); ok && l == nil {
|
// The bindings all fail closed without a VM, so this would be harmless off
|
||||||
looper_prepare()
|
// Android -- just a pointless chain of JNI calls on every tap. Compile it
|
||||||
}
|
// out instead of relying on that.
|
||||||
|
when ODIN_PLATFORM_SUBTARGET == .Android {
|
||||||
|
if l, ok := looper_my_looper(); ok && l == nil {
|
||||||
|
looper_prepare()
|
||||||
|
}
|
||||||
|
|
||||||
toast, ok := toast_make_text(toast_ctx, msg, 0) // 0 = Toast.LENGTH_SHORT
|
toast, ok := toast_make_text(toast_ctx, msg, 0) // 0 = Toast.LENGTH_SHORT
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
toast_show(toast)
|
||||||
}
|
}
|
||||||
toast_show(toast)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Walks Activity -> Window -> decor View -> WindowInsets. Returns false while
|
// Walks Activity -> Window -> decor View -> WindowInsets. Returns false while
|
||||||
// the decor view is still unattached, which is normal for the first few frames.
|
// the decor view is still unattached, which is normal for the first few frames.
|
||||||
refresh_insets :: proc() -> bool {
|
refresh_insets :: proc() -> bool {
|
||||||
|
// Returning true, not false: zero insets are the correct answer where there
|
||||||
|
// are no system bars, and the caller retries every frame until this succeeds
|
||||||
|
// -- requesting a render each time. Failing here would pin the desktop loop
|
||||||
|
// awake forever, which is exactly what render_on_change is meant to avoid.
|
||||||
|
when ODIN_PLATFORM_SUBTARGET != .Android {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// This runs every frame until it succeeds, and each call below returns a
|
// This runs every frame until it succeeds, and each call below returns a
|
||||||
// local ref. Without a frame to scope them the ref table fills and aborts.
|
// local ref. Without a frame to scope them the ref table fills and aborts.
|
||||||
env := jni.env()
|
env := jni.env()
|
||||||
|
|||||||
Reference in New Issue
Block a user