ffi #jni_main R.5: retire legacy NativeActivity surface

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.
This commit is contained in:
agra
2026-05-20 15:17:24 +03:00
parent 3300bfb0df
commit 619d524bac
5 changed files with 10 additions and 737 deletions

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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 `<jni.h>`:
// 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
// `<android/native_activity.h>` 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;
}

View File

@@ -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

View File

@@ -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 <android/input.h>
// 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