From 619d524bac9c3374910a555dce967d33a154be33 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 20 May 2026 15:17:24 +0300 Subject: [PATCH] ffi #jni_main R.5: retire legacy NativeActivity surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes the entire NativeActivity / native_app_glue / ALooper stack that the previous Android entry path was built around: - `examples/99-android-egl-clear.sx` — the demo of the legacy path. - `library/modules/platform/android.sx` — `AndroidPlatform.init`, `run_frame_loop`, `sx_android_bootstrap`, `g_android_app`, plus the ALooper / AInputEvent / ANativeActivity / AConfiguration foreign decls that fed them. The JNI helpers (`sx_load_javavm_fn`, `sx_android_get_env`, `sx_query_safe_insets_jni`, the `ANATIVEACTIVITY_*` offsets) were tied to the ANativeActivity* delivered to `android_main` — they're stale now that the OS hands sx code a Java Activity directly via `onCreate(JNIEnv*, jobject)`. - `library/vendors/sx_android_jni/sx_android_jni.c` — the input- handler installer (`sx_android_install_input_handler`), which poked NDK app-pointer field offsets that no longer exist. `library/modules/platform/android_jni.sx` (the `Activity`/`Window`/ `View`/`WindowInsets` `#jni_class` registry used for safe-insets dispatch) survives — it's standalone declarative bindings useful from any `#jni_main` onCreate body. Docstring updated to drop the "imported from android.sx" framing. 131 host / 4 cross / zig build test all green. End-to-end smoke APK still produces the expected JNI-mangled symbol + SxApp-extends-Activity dex. External consumers (chess) will need to migrate their entry from the `AndroidPlatform.run_frame_loop` model to the `#jni_main` model (Java-side Activity drives lifecycle; onSurfaceChanged / Choreographer drive frames via JNI callbacks). That migration is downstream work. --- examples/99-android-egl-clear.sx | 79 --- library/modules/gpu/gles3.sx | 3 +- library/modules/platform/android.sx | 615 ------------------ library/modules/platform/android_jni.sx | 16 +- .../vendors/sx_android_jni/sx_android_jni.c | 34 - 5 files changed, 10 insertions(+), 737 deletions(-) delete mode 100644 examples/99-android-egl-clear.sx delete mode 100644 library/modules/platform/android.sx delete mode 100644 library/vendors/sx_android_jni/sx_android_jni.c diff --git a/examples/99-android-egl-clear.sx b/examples/99-android-egl-clear.sx deleted file mode 100644 index 5cd059d..0000000 --- a/examples/99-android-egl-clear.sx +++ /dev/null @@ -1,79 +0,0 @@ -// Android-only: pure-sx NativeActivity that brings up EGL on the -// ANativeWindow delivered by native_app_glue and clears the screen -// every frame via GLES3. Equivalent of `examples/63-metal-clear.sx` -// for the Android target. -// -// Entry-point contract (the "via Platform" shape): -// - User writes BOTH `main` and `android_main` at top level. -// - `android_main(app)` calls `sx_android_bootstrap(app)` and then -// `main()`. The library never names `main`; the OS-shape entry -// symbol lives in user code, where the other entry symbols are. -// - `main` instantiates `AndroidPlatform`, calls `init`, then -// `run_frame_loop` which drives the looper until destroyRequested. -// -// This exercises end-to-end the Android pipeline shipped in Session 70: -// - `sx build --target android` toolchain (NDK clang + glue link). -// - `--apk` APK assembly (manifest + aapt2 + zipalign + apksigner). -// - `android_main` (user-written here) gets external LLVM linkage -// via the `isExportedEntryName` allowlist in lower.zig. -// - `AndroidPlatform.run_frame_loop` drains ALooper events, -// creates EGL on `APP_CMD_INIT_WINDOW`, ticks the closure every -// 16 ms. -// -// Build + install on a connected device: -// -// /Users/agra/projects/sx/zig-out/bin/sx build --target android \ -// --apk /tmp/sxhello.apk --bundle-id co.swipelab.sxhello \ -// -o /tmp/libsxhello.so examples/99-android-egl-clear.sx -// adb install -r /tmp/sxhello.apk -// adb shell am start -n co.swipelab.sxhello/android.app.NativeActivity -// adb logcat -d --pid=$(adb shell pidof co.swipelab.sxhello) -// -// Expected: solid purple frame on the device. Periodic -// `rendered 60 frames` lines in logcat. - -#import "modules/std.sx"; -#import "modules/compiler.sx"; -#import "modules/platform/api.sx"; -#import "modules/platform/android.sx"; - -// GLES3 (linked via -lGLESv3) -glClearColor :: (r: f32, g: f32, b: f32, a: f32) #foreign; -glClear :: (mask: u32) #foreign; -GL_COLOR_BUFFER_BIT :u32: 0x4000; - -frame_count : s32 = 0; -g_plat : *AndroidPlatform = null; - -frame_tick :: () { - fc := g_plat.begin_frame(); - glClearColor(0.5, 0.2, 0.8, 1.0); // purple - glClear(GL_COLOR_BUFFER_BIT); - g_plat.end_frame(); - - frame_count += 1; - if (frame_count % 60) == 0 { - __android_log_print(4, "sxhello".ptr, "rendered 60 frames\n".ptr); - } -} - -main :: () -> s32 { - inline if OS == .android { - plat : AndroidPlatform = .{}; - plat.init("sxhello", 0, 0); - g_plat = @plat; - plat.run_frame_loop(() => frame_tick()); - } - 0; -} - -// OS-shape entry symbol. native_app_glue's -// `ANativeActivity_onCreate` ultimately calls this on the worker -// thread. We hand the app pointer to the platform module and then -// let user `main` drive the normal cross-platform setup path. -android_main :: (app: *void) { - inline if OS == .android { - sx_android_bootstrap(app); - main(); - } -} diff --git a/library/modules/gpu/gles3.sx b/library/modules/gpu/gles3.sx index f92a63a..ed2a1ea 100644 --- a/library/modules/gpu/gles3.sx +++ b/library/modules/gpu/gles3.sx @@ -28,7 +28,8 @@ // EGL bootstrap helper — used to populate opengl.sx's fn pointers once // the EGL context is current. We declare only the loader here; EGL -// surface/context creation lives in platform/android.sx. +// surface/context creation belongs in the consumer's `#jni_main` +// Activity (e.g. an `onCreate` body that calls `eglCreateContext`). eglGetProcAddress :: (name: [*]u8) -> *void #foreign; diff --git a/library/modules/platform/android.sx b/library/modules/platform/android.sx deleted file mode 100644 index 12aec5e..0000000 --- a/library/modules/platform/android.sx +++ /dev/null @@ -1,615 +0,0 @@ -// Pure-NDK NativeActivity backend for Android. -// -// Linking is per-target via the consumer's build.sx — for Android we -// don't need explicit framework adds because `sx build --target android` -// already links -llog -landroid -lEGL -lGLESv3 and bundles -// native_app_glue.c. The file compiles cleanly on every target because -// every NDK-touching path is gated by `inline if OS == .android`. -// -// Entry-point contract (deliberately kept symmetric with iOS): -// - The user writes both `main` and `android_main` at top level. -// - The user's `android_main(app)` calls `sx_android_bootstrap(app)` -// once (to stash the NDK app pointer in this module's globals), -// then calls `main()`. The body of `android_main` should be -// `inline if OS == .android`-gated; on non-Android targets it -// compiles to dead code, harmless because nothing references the -// symbol. -// - User's `main` constructs an `AndroidPlatform`, calls `init`, -// then `run_frame_loop(closure)` which drives the looper until -// destroyRequested. This file never names `main`. - -#import "modules/std.sx"; -#import "modules/compiler.sx"; -#import "modules/ui/types.sx"; -#import "modules/ui/events.sx"; -#import "modules/platform/types.sx"; -#import "modules/platform/api.sx"; - -// JNI bridge for system-bar inset queries. The .c lives in the library's -// vendor area; the compiler resolves the `#source` path through the -// stdlib search list so consumers don't need to vendor a copy. -#import c { - #source "vendors/sx_android_jni/sx_android_jni.c"; -}; - -// ── Foreign declarations ──────────────────────────────────────────────── - -__android_log_print :: (prio: s32, tag: *u8, fmt: *u8) -> s32 #foreign; -usleep :: (us: u32) -> s32 #foreign; - -ALooper_pollOnce :: (ms: s32, outFd: *s32, outEvents: *s32, outData: **void) -> s32 #foreign; - -ANativeWindow_getWidth :: (window: *void) -> s32 #foreign; -ANativeWindow_getHeight :: (window: *void) -> s32 #foreign; - -AConfiguration_getDensity :: (config: *void) -> s32 #foreign; - -// Input event APIs (libandroid). Touch motion arrives via the queued -// AInputEvent stream that native_app_glue pumps through `onInputEvent`. -AInputEvent_getType :: (event: *void) -> s32 #foreign; -AMotionEvent_getAction :: (event: *void) -> s32 #foreign; -AMotionEvent_getX :: (event: *void, pointer_index: u64) -> f32 #foreign; -AMotionEvent_getY :: (event: *void, pointer_index: u64) -> f32 #foreign; - -// Glue bridge from vendors/sx_android_jni/sx_android_jni.c. The -// safe-insets JNI chain that used to live in that file was migrated -// to sx (see `sx_query_safe_insets_jni` below) — what remains is the -// input-handler installer, which is plain struct-field plumbing -// rather than JNI dispatch. -sx_android_install_input_handler :: (app: *void, handler: (*void, *void) -> s32) -> void #foreign; - -// JavaVM vtable indirection — used to attach the calling thread to -// the JVM and recover a `JNIEnv*` for it. `#jni_call` only handles -// `JNIEnv*` dispatch (a different vtable), so the JavaVM hop is -// hand-rolled here. -// -// Slot indices match `JNIInvokeInterface_` in ``: -// 3 DestroyJavaVM, 4 AttachCurrentThread, 5 DetachCurrentThread, -// 6 GetEnv, 7 AttachCurrentThreadAsDaemon. -JNI_VERSION_1_6 :: 0x00010006; - -// Byte offsets into `ANativeActivity` on 64-bit Android (the only -// Android ABI we target). `vm` is the JavaVM*, `clazz` is the -// activity's jobject. The C struct layout in -// `` is the source of truth; these -// offsets MUST track that. -ANATIVEACTIVITY_VM_OFFSET :: 8; -ANATIVEACTIVITY_CLAZZ_OFFSET :: 24; - -// Load a `*void` field at `base + byte_offset`. Plumbing for raw -// struct access from foreign pointers. -sx_load_ptr_at :: (base: *void, byte_offset: usize) -> *void { - addr : usize = xx base; - slot : **void = xx (addr + byte_offset); - slot.*; -} - -// Load a JavaVM function pointer at the given vtable slot index. -// `vm` is the `JavaVM*` (which points to `JNIInvokeInterface*`), so -// the indirection is `*vm + slot * sizeof(ptr)`. -sx_load_javavm_fn :: (vm: *void, slot: usize) -> *void { - vtable_pp : **void = xx vm; - vtable : *void = vtable_pp.*; - addr : usize = xx vtable; - fn_slot : **void = xx (addr + slot * 8); - fn_slot.*; -} - -// Attach the current thread to the JVM if needed, hand back a -// `JNIEnv*`. `out_attached` is set to true when this call had to do -// the attach (caller should match with `sx_android_detach_env`). -// Returns null on failure (no VM, or attach refused). -sx_android_get_env :: (activity: *void, out_attached: *bool) -> *void { - inline if OS != .android { return null; } - out_attached.* = false; - if activity == null { return null; } - vm := sx_load_ptr_at(activity, ANATIVEACTIVITY_VM_OFFSET); - if vm == null { return null; } - - env : *void = null; - get_env_fn : (*void, **void, s32) -> s32 = xx sx_load_javavm_fn(vm, 6); - if get_env_fn(vm, @env, xx JNI_VERSION_1_6) == 0 { return env; } - - attach_fn : (*void, **void, *void) -> s32 = xx sx_load_javavm_fn(vm, 4); - if attach_fn(vm, @env, null) != 0 { return null; } - out_attached.* = true; - env; -} - -sx_android_detach_env :: (activity: *void) { - inline if OS != .android { return; } - if activity == null { return; } - vm := sx_load_ptr_at(activity, ANATIVEACTIVITY_VM_OFFSET); - if vm == null { return; } - detach_fn : (*void) -> s32 = xx sx_load_javavm_fn(vm, 5); - detach_fn(vm); -} - -// Read the activity's `clazz` jobject (the Java-side activity -// reference). Wrapper around the raw byte-offset load so the call -// site reads naturally. -sx_android_activity_clazz :: (activity: *void) -> *void { - inline if OS != .android { return null; } - if activity == null { return null; } - sx_load_ptr_at(activity, ANATIVEACTIVITY_CLAZZ_OFFSET); -} - -// Declarative JNI class bindings for the safe-insets dispatch chain -// live in a named sub-module so they don't collide with consumer-side -// types (e.g. `modules/ui/view.sx`'s `View` protocol). The compiler -// registers the foreign-class decls inside under both qualified -// (`Jni.Activity`) and bare (`Activity`) names — receiver types use -// the qualified form, cross-class refs in method signatures use the -// bare form. -Jni :: #import "modules/platform/android_jni.sx"; - -// sx-side reimplementation of the JNI dispatch chain. Caller provides -// an already-attached `JNIEnv*` and the activity's `clazz` jobject -// (cast to `*Jni.Activity` so the method-call DSL can route it through -// the foreign-class registry). Outputs physical-pixel insets. -sx_query_safe_insets_jni :: (env: *void, activity: *Jni.Activity, top: *s32, left: *s32, bottom: *s32, right: *s32) -> void { - inline if OS != .android { return; } - - top.* = 0; left.* = 0; bottom.* = 0; right.* = 0; - if activity == null { return; } - - #jni_env(env) { - window := activity.getWindow(); - if window == null { return; } - - decor := window.getDecorView(); - if decor == null { return; } - - insets := decor.getRootWindowInsets(); - if insets == null { return; } - - top.* = insets.getSystemWindowInsetTop(); - left.* = insets.getSystemWindowInsetLeft(); - bottom.* = insets.getSystemWindowInsetBottom(); - right.* = insets.getSystemWindowInsetRight(); - } -} - -// EGL — display/surface/context/config are opaque to us. -eglGetDisplay :: (display_id: *void) -> *void #foreign; -eglInitialize :: (display: *void, major: *s32, minor: *s32) -> u32 #foreign; -eglChooseConfig :: (display: *void, attrib_list: *s32, configs: **void, config_size: s32, num_config: *s32) -> u32 #foreign; -eglCreateContext :: (display: *void, config: *void, share: *void, attrib_list: *s32) -> *void #foreign; -eglCreateWindowSurface:: (display: *void, config: *void, window: *void, attrib_list: *s32) -> *void #foreign; -eglMakeCurrent :: (display: *void, draw: *void, read: *void, ctx: *void) -> u32 #foreign; -eglSwapBuffers :: (display: *void, surface: *void) -> u32 #foreign; -eglDestroySurface :: (display: *void, surface: *void) -> u32 #foreign; -eglDestroyContext :: (display: *void, ctx: *void) -> u32 #foreign; -eglTerminate :: (display: *void) -> u32 #foreign; - -clock_gettime :: (clk_id: s32, ts: *void) -> s32 #foreign; - -// ── Constants ────────────────────────────────────────────────────────── - -EGL_NONE :s32: 0x3038; -EGL_SURFACE_TYPE :s32: 0x3033; -EGL_WINDOW_BIT :s32: 0x0004; -EGL_RENDERABLE_TYPE :s32: 0x3040; -EGL_OPENGL_ES2_BIT :s32: 0x0004; -EGL_OPENGL_ES3_BIT :s32: 0x0040; -EGL_BLUE_SIZE :s32: 0x3022; -EGL_GREEN_SIZE :s32: 0x3023; -EGL_RED_SIZE :s32: 0x3024; -EGL_DEPTH_SIZE :s32: 0x3025; -EGL_CONTEXT_CLIENT_VERSION :s32: 0x3098; - -// AInputEvent / AMotionEvent constants used by the input handler. -AINPUT_EVENT_TYPE_MOTION :s32: 2; -AMOTION_EVENT_ACTION_MASK :s32: 0xff; -AMOTION_EVENT_ACTION_DOWN :s32: 0; -AMOTION_EVENT_ACTION_UP :s32: 1; -AMOTION_EVENT_ACTION_MOVE :s32: 2; -AMOTION_EVENT_ACTION_CANCEL :s32: 3; - -// CLOCK_MONOTONIC = 1 on linux/bionic. Used for delta_time + target_present_time. -CLOCK_MONOTONIC :s32: 1; - -// android_app + android_poll_source field offsets (NDK 29, arm64). -// Recompute via offsetof() on a host with the NDK headers if you suspect -// the layout has changed (see examples/99-android-egl-clear.sx's notes). -APP_OFF_window :s64: 72; -APP_OFF_destroyRequested :s64: 100; -APP_OFF_config :s64: 32; // AConfiguration* — follows activity (24). -SRC_OFF_process :s64: 16; // id(4) + pad(4) + app*(8) = process fn-ptr - -// timespec on linux/aarch64: tv_sec (s64) + tv_nsec (s64). -TimeSpec :: struct { sec: s64; nsec: s64; } - -// ── Globals ──────────────────────────────────────────────────────────── -// `g_android_app` is populated by `sx_android_bootstrap` before -// `AndroidPlatform.run_frame_loop` reads it. `g_android_plat` is set -// by `AndroidPlatform.init` so future hooks (signal handlers, JNI -// callbacks) can find the live platform. - -g_android_app : *void = null; -g_android_plat : *AndroidPlatform = null; - -// `app->activity` (ANativeActivity*) at byte 24. -// `activity->assetManager` at byte 64 inside ANativeActivity. -APP_OFF_activity :s64: 24; -ACTIVITY_OFF_assetManager :s64: 64; -ACTIVITY_OFF_internalData :s64: 32; - -// AAssetManager handle the user's `android_main` stashes via the -// bootstrap. Consumers that want to read bundled APK assets (font.ttf, -// piece sprites, level data, ...) read this and feed it into their own -// file-IO shim — e.g. chess wires it through `vendors/file_utils.c` -// which routes `read_file_bytes` through AAssetManager_open on Android. -g_android_asset_manager : *void = null; -g_android_internal_path : *u8 = null; -g_android_config : *void = null; -g_android_activity : *void = null; - -// ── Bootstrap (called by user's `android_main`) ──────────────────────── -// Stashes the NDK app pointer the OS handed to `android_main(app)` so -// the rest of the platform module can find it. Single responsibility — -// the user's `android_main` calls this once, then calls their own -// `main()` to enter the normal cross-platform setup flow. -// -// Also extracts `AAssetManager*` and `internalDataPath` from the -// ANativeActivity that the app pointer carries — these are the two -// pieces consumers need to touch APK assets / per-app storage on -// Android. Reachable as `g_android_asset_manager` / `g_android_internal_path`. - -sx_android_bootstrap :: (app: *void) { - inline if OS == .android { - g_android_app = app; - base : s64 = xx app; - activity_pp : **void = xx (base + APP_OFF_activity); - activity_ptr := activity_pp.*; - g_android_activity = activity_ptr; - if activity_ptr != null { - act_base : s64 = xx activity_ptr; - mgr_pp : **void = xx (act_base + ACTIVITY_OFF_assetManager); - g_android_asset_manager = mgr_pp.*; - path_pp : **u8 = xx (act_base + ACTIVITY_OFF_internalData); - g_android_internal_path = path_pp.*; - } - cfg_pp : **void = xx (base + APP_OFF_config); - g_android_config = cfg_pp.*; - } -} - -// ── Helpers ──────────────────────────────────────────────────────────── - -read_ptr :: (base: s64, off: s64) -> *void { - inline if OS != .android { return null; } - p : **void = xx (base + off); - p.*; -} - -read_s32 :: (base: s64, off: s64) -> s32 { - inline if OS != .android { return 0; } - p : *s32 = xx (base + off); - p.*; -} - -monotonic_seconds :: () -> f64 { - inline if OS != .android { return 0.0; } - ts : TimeSpec = .{}; - clock_gettime(CLOCK_MONOTONIC, xx @ts); - (xx ts.sec) + (xx ts.nsec) / 1000000000.0; -} - -// ── Platform implementation ──────────────────────────────────────────── - -AndroidPlatform :: struct { - // EGL state — created when ANativeWindow arrives via the event loop. - egl_display: *void = null; - egl_surface: *void = null; - egl_context: *void = null; - egl_config: *void = null; - - // Latest known dimensions reported by the window. Refreshed each frame - // from ANativeWindow_getWidth/Height — cheap and avoids missing resize - // events when the user rotates the device. - pixel_w: s32 = 0; - pixel_h: s32 = 0; - dpi_scale: f32 = 1.0; - delta_time: f32 = 0.016; - last_frame_time: f64 = 0.0; - - // User's per-frame closure stored when run_frame_loop is called. - // Optional sentinel-shape — `null` means "no closure yet". - frame_closure: ?Closure() = null; - - events: List(Event) = .{}; - last_touch: Point = .{}; - touch_active: bool = false; - stop_requested: bool = false; - - safe_top: f32 = 0.0; - safe_left: f32 = 0.0; - safe_bottom: f32 = 0.0; - safe_right: f32 = 0.0; - safe_insets_queried: bool = false; - - keyboard_visible: bool = false; - keyboard_height: f32 = 0.0; -} - -impl Platform for AndroidPlatform { - // title/w/h are advisory only — the OS owns the surface dimensions. - // We register the platform globally so android_main path can find it. - init :: (self: *AndroidPlatform, title: [:0]u8, w: s32, h: s32) -> bool { - inline if OS != .android { return false; } - g_android_plat = self; - if g_android_config != null { - density := AConfiguration_getDensity(g_android_config); - if density > 0 { - self.dpi_scale = xx density / 160.0; - } - } - true; - } - - run_frame_loop :: (self: *AndroidPlatform, frame_fn: Closure()) { - inline if OS == .android { - self.frame_closure = frame_fn; - if g_android_app != null { - sx_android_install_input_handler(g_android_app, sx_android_input_event); - } - android_run_loop(self); - } - } - - poll_events :: (self: *AndroidPlatform) -> []Event { - out : []Event = .{ ptr = xx self.events.items, len = self.events.len }; - // Drain after exposing the slice — the user iterates synchronously - // before returning to the run loop, and new touches won't arrive - // until the next ALooper_pollOnce drains the input queue. Setting - // len=0 keeps the backing buffer alive so the next append reuses - // the same allocation. - self.events.len = 0; - out; - } - - begin_frame :: (self: *AndroidPlatform) -> FrameContext { - viewport_w_f : f32 = xx self.pixel_w; - viewport_h_f : f32 = xx self.pixel_h; - if self.dpi_scale > 0.0 { - viewport_w_f = viewport_w_f / self.dpi_scale; - viewport_h_f = viewport_h_f / self.dpi_scale; - } - FrameContext.{ - viewport_w = viewport_w_f, - viewport_h = viewport_h_f, - pixel_w = self.pixel_w, - pixel_h = self.pixel_h, - dpi_scale = self.dpi_scale, - delta_time = self.delta_time, - target_present_time = 0.0, - }; - } - - end_frame :: (self: *AndroidPlatform) { - inline if OS == .android { - if self.egl_surface != null { - eglSwapBuffers(self.egl_display, self.egl_surface); - } - } - } - - safe_insets :: (self: *AndroidPlatform) -> EdgeInsets { - inline if OS == .android { - // Query once after EGL is up — getRootWindowInsets() returns - // null until the window has been attached, so calling at - // bootstrap is too early. - if !self.safe_insets_queried and g_android_activity != null and self.egl_surface != null { - t : s32 = 0; l : s32 = 0; b : s32 = 0; r : s32 = 0; - attached : bool = false; - env := sx_android_get_env(g_android_activity, @attached); - if env != null { - clazz : *Jni.Activity = xx sx_android_activity_clazz(g_android_activity); - sx_query_safe_insets_jni(env, clazz, @t, @l, @b, @r); - if attached { sx_android_detach_env(g_android_activity); } - } - inv : f32 = if self.dpi_scale > 0.0 then 1.0 / self.dpi_scale else 1.0; - self.safe_top = xx t * inv; - self.safe_left = xx l * inv; - self.safe_bottom = xx b * inv; - self.safe_right = xx r * inv; - self.safe_insets_queried = true; - } - } - EdgeInsets.{ - top = self.safe_top, - left = self.safe_left, - bottom = self.safe_bottom, - right = self.safe_right, - }; - } - - keyboard :: (self: *AndroidPlatform) -> KeyboardState { - KeyboardState.{ - visible = self.keyboard_visible, - height = self.keyboard_height, - }; - } - - show_keyboard :: (self: *AndroidPlatform) { - // TODO: InputMethodManager.showSoftInput via JNI. - } - - hide_keyboard :: (self: *AndroidPlatform) { - // TODO: InputMethodManager.hideSoftInputFromWindow via JNI. - } - - stop :: (self: *AndroidPlatform) { - self.stop_requested = true; - } - - shutdown :: (self: *AndroidPlatform) { - inline if OS == .android { - if self.egl_display != null { - eglMakeCurrent(self.egl_display, null, null, null); - if self.egl_surface != null { - eglDestroySurface(self.egl_display, self.egl_surface); - } - if self.egl_context != null { - eglDestroyContext(self.egl_display, self.egl_context); - } - eglTerminate(self.egl_display); - self.egl_display = null; - self.egl_surface = null; - self.egl_context = null; - } - } - } -} - -// ── Internal: input bridge ───────────────────────────────────────────── -// -// native_app_glue's process_input loop calls this once per AInputEvent -// pulled off the input queue. Single touch only for now — point-down / -// move / point-up — translated to sx mouse_down / mouse_moved / -// mouse_up so the existing handle_event chain (drag, scroll, square -// selection) Just Works. Coordinates from AMotionEvent_get{X,Y} are -// physical pixels; divide by dpi_scale before publishing so the -// layout-side hit-testing matches its own logical-coord frames. -// -// Returns 1 (consumed) for motion events, 0 for everything else so -// native_app_glue still routes key events through the default handler. - -sx_android_input_event :: (app: *void, event: *void) -> s32 { - inline if OS != .android { return 0; } - if event == null { return 0; } - if g_android_plat == null { return 0; } - - if AInputEvent_getType(event) != AINPUT_EVENT_TYPE_MOTION { - return 0; - } - - plat := g_android_plat; - raw_action := AMotionEvent_getAction(event); - action := raw_action & AMOTION_EVENT_ACTION_MASK; - - px := AMotionEvent_getX(event, 0); - py := AMotionEvent_getY(event, 0); - inv : f32 = if plat.dpi_scale > 0.0 then 1.0 / plat.dpi_scale else 1.0; - pos : Point = .{ x = px * inv, y = py * inv }; - - if action == AMOTION_EVENT_ACTION_DOWN { - plat.events.append(.mouse_down(.{ position = pos, button = .left })); - plat.last_touch = pos; - plat.touch_active = true; - } else if action == AMOTION_EVENT_ACTION_MOVE { - delta : Point = .{ - x = pos.x - plat.last_touch.x, - y = pos.y - plat.last_touch.y, - }; - plat.events.append(.mouse_moved(.{ position = pos, delta = delta })); - plat.last_touch = pos; - } else if action == AMOTION_EVENT_ACTION_UP { - plat.events.append(.mouse_up(.{ position = pos, button = .left })); - plat.touch_active = false; - } else if action == AMOTION_EVENT_ACTION_CANCEL { - plat.events.append(.mouse_up(.{ position = pos, button = .left })); - plat.touch_active = false; - } - 1; -} - -// ── Internal: event loop + EGL bringup ───────────────────────────────── - -android_run_loop :: (self: *AndroidPlatform) { - inline if OS != .android { return; } - if g_android_app == null { return; } - - app_base : s64 = xx g_android_app; - out_fd : s32 = 0; - out_events : s32 = 0; - out_data : *void = null; - - self.last_frame_time = monotonic_seconds(); - - while self.stop_requested == false { - // Drain pending events. Non-blocking pollOnce is mandatory on - // Pixel 7 Pro + Android 16 — pollOnce(-1) blows the stack inside - // Looper::pollOnce there (see examples/99-android-egl-clear.sx's - // notes for the investigation). - ret := ALooper_pollOnce(0, @out_fd, @out_events, @out_data); - if ret >= 0 { - if out_data != null { - src_base : s64 = xx out_data; - process_addr := read_ptr(src_base, SRC_OFF_process); - if process_addr != null { - process_fn : (*void, *void) -> void = xx process_addr; - process_fn(g_android_app, out_data); - } - } - } - - if read_s32(app_base, APP_OFF_destroyRequested) != 0 { break; } - - window := read_ptr(app_base, APP_OFF_window); - if window != null and self.egl_surface == null { - if android_setup_egl(self, window) == false { break; } - } - - if self.egl_surface != null { - // Refresh dimensions every tick — handles rotations cleanly. - self.pixel_w = ANativeWindow_getWidth(window); - self.pixel_h = ANativeWindow_getHeight(window); - - now := monotonic_seconds(); - dt : f64 = now - self.last_frame_time; - if dt > 0.0 { self.delta_time = xx dt; } - self.last_frame_time = now; - - // `if let`-style unwrap: only invoke if the closure was set. - // User's frame closure is expected to call plat.end_frame() - // which swaps — matches uikit.sx's begin/end contract. - if frame_fn := self.frame_closure { - frame_fn(); - } - } - - usleep(16000); - } -} - -android_setup_egl :: (self: *AndroidPlatform, window: *void) -> bool { - inline if OS != .android { return false; } - - self.egl_display = eglGetDisplay(null); - if self.egl_display == null { return false; } - - major : s32 = 0; - minor : s32 = 0; - if eglInitialize(self.egl_display, @major, @minor) == 0 { return false; } - - attribs : [13]s32 = .{ - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, - EGL_BLUE_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_RED_SIZE, 8, - EGL_DEPTH_SIZE, 0, - EGL_NONE, - }; - num_config : s32 = 0; - if eglChooseConfig(self.egl_display, @attribs[0], @self.egl_config, 1, @num_config) == 0 or num_config < 1 { - return false; - } - - ctx_attribs : [3]s32 = .{ EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; - self.egl_context = eglCreateContext(self.egl_display, self.egl_config, null, @ctx_attribs[0]); - if self.egl_context == null { return false; } - - self.egl_surface = eglCreateWindowSurface(self.egl_display, self.egl_config, window, null); - if self.egl_surface == null { return false; } - - if eglMakeCurrent(self.egl_display, self.egl_surface, self.egl_surface, self.egl_context) == 0 { - return false; - } - - self.pixel_w = ANativeWindow_getWidth(window); - self.pixel_h = ANativeWindow_getHeight(window); - true; -} diff --git a/library/modules/platform/android_jni.sx b/library/modules/platform/android_jni.sx index 728a5cf..22e27b3 100644 --- a/library/modules/platform/android_jni.sx +++ b/library/modules/platform/android_jni.sx @@ -1,11 +1,11 @@ -// Declarative JNI class bindings used by the Android platform module's -// safe-insets dispatch chain. Imported under a named namespace from -// `modules/platform/android.sx` so the bare class names (`Activity`, -// `Window`, `View`, `WindowInsets`) don't pollute the top-level -// namespace when consumers flat-import the platform module — `View` -// in particular collides with `modules/ui/view.sx`'s protocol. -// -// Inside the platform module these are referenced as `Jni.Activity`, +// Declarative JNI class bindings for the standard Android system-bar +// inset chain (`Activity.getWindow → Window.getDecorView → View +// .getRootWindowInsets → WindowInsets.getSystemWindowInset{Top,Left, +// Bottom,Right}`). Intended to be imported under a named namespace +// (e.g. `Jni :: #import "library/modules/platform/android_jni.sx"`) so +// the bare class names don't pollute the top-level namespace — +// `View` in particular collides with `modules/ui/view.sx`'s protocol. +// Inside the namespace these are referenced as `Jni.Activity`, // `Jni.Window`, etc. The compiler registers the decls both qualified // and bare in `foreign_class_map`, so cross-class refs in method // signatures (`getWindow :: (self: *Self) -> *Window`) still resolve diff --git a/library/vendors/sx_android_jni/sx_android_jni.c b/library/vendors/sx_android_jni/sx_android_jni.c deleted file mode 100644 index c1f77cc..0000000 --- a/library/vendors/sx_android_jni/sx_android_jni.c +++ /dev/null @@ -1,34 +0,0 @@ -// JNI helpers used by modules/platform/android.sx. Kept in the library -// so consumers don't need to vendor an identically-named copy. The sx -// compiler resolves `#source "vendors/..."` against the stdlib search -// paths in addition to the consumer's project root. -// -// The safe-insets JNI chain that used to live here was migrated to -// sx in Phase 1D of the FFI plan (see `library/modules/platform/ -// android.sx::sx_query_safe_insets_jni` and the JavaVM helpers -// alongside it). What remains is the input-handler installer, which -// is a plain C struct-field assignment rather than JNI dispatch and -// has no sx equivalent yet. - -#ifdef __ANDROID__ -#include - -// Mirror of struct android_app (NDK 29 / arm64) up to the fields we touch. -// Avoids depending on the glue header in the sx library compile path. -struct sx_android_app_min { - void* userData; - void (*onAppCmd)(struct sx_android_app_min* app, int cmd); - int (*onInputEvent)(struct sx_android_app_min* app, AInputEvent* event); - // ...rest of struct ignored; we only assign onInputEvent. -}; - -// Install an sx-side handler as `app->onInputEvent`. native_app_glue's -// process_input loop calls this for every AInputEvent it pulls off the -// input queue. Returning 1 marks the event as consumed. -void sx_android_install_input_handler(void* app, - int (*handler)(void* app, void* event)) { - if (app == 0) return; - struct sx_android_app_min* a = (struct sx_android_app_min*)app; - a->onInputEvent = (int (*)(struct sx_android_app_min*, AInputEvent*))handler; -} -#endif