Files
sx/library/modules/platform/android.sx
agra cc29cfa7ce ffi #jni_main: jni_java_emit + android.sx + manifest fixes; chess on Pixel
Combined slice — gets chess rendering on a Pixel 7 Pro via the
`#jni_main` pipeline. Half-dozen jni_java_emit fixes plus the rebuilt
stdlib android module:

  jni_java_emit:
    - `#implements Alias;` body members render as Java `implements`
      clauses on the class header (space-separated, registry-resolved).
    - Drop the implicit `super.<method>(args)` call from the @Override
      delegate — interface impls (SurfaceHolder.Callback) have no
      super; user calls super explicitly from sx-side via
      `super.method(args)` lowered to `CallNonvirtual<T>Method`.
    - `static { System.loadLibrary("<libname>"); }` static init block,
      lib name derived from the build's `-o` basename.
    - `name: Type;` body items render as private Java fields.
    - `$` (JNI nested-class shape) → `.` in Java source: e.g.
      `android/view/SurfaceHolder$Callback` → `android.view.SurfaceHolder.Callback`.
    - Non-void @Override bodies `return` the native delegate's result.

  lower.zig:
    - `super.method(args)` sugar inside a `#jni_main` (or any
      sx-defined `#jni_class`) bodied method lowers to JNI
      `CallNonvirtual<T>Method` with the parent class resolved via
      `#extends` (default Activity).
    - `Alias.new(args)` constructor sugar lowers to JNI
      `FindClass + GetMethodID("<init>", sig) + NewObject`.
    - `jniMapParamType` stops erasing pointer types so method dispatch
      on foreign-class params (`holder.getSurface()`) resolves.
    - synthesizeJniMainStub pushes the env arg onto the lexical
      `#jni_env` stack so omitted-env `#jni_call` and `super.method`
      sites see it.

  target.zig:
    - Manifest synthesised from `#jni_main` adds
      `android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"`
      so sx apps own the whole window (no title strip, no status bar).

  library/modules/platform/android.sx (NEW):
    - Replaces the retired NativeActivity-based module under #jni_main.
    - Foreign-class decls for Bundle / Context / Surface / SurfaceHolder
      / SurfaceView / MotionEvent / View / Activity / SurfaceHolderCallback /
      AssetManagerJ.
    - libandroid / EGL / pthread foreign C decls.
    - Helpers consumers call from their Activity body:
      `sx_android_forward_assets(env, ctx)`,
      `sx_android_attach_window(env, holder)`,
      `sx_android_detach_window()`,
      `sx_android_set_viewport(w, h)`,
      `sx_android_start_render_thread(main_fn)`,
      `sx_android_push_touch(action, x, y)`.
    - Render thread brings up EGL on the ANativeWindow then calls the
      user-supplied entry fn pointer.
    - `AndroidPlatform` struct + `impl Platform` (init / begin_frame /
      end_frame / poll_events / safe_insets / keyboard / show_keyboard /
      hide_keyboard / stop / shutdown / run_frame_loop).

End-to-end verified on a Pixel 7 Pro: chess APK builds via
`sx build --target android --apk ... --bundle-id ... -o ...`, installs
via `adb install -r`, launches and renders the chess board with all
pieces in starting position. No title strip, no flicker. Touch events
reach `sx_android_push_touch` and drain through `poll_events` (debug-
verified) — chess's pipeline-side hit-test routing + DPI-correct
sizing remain as follow-ups.

138 host / 8 cross / `zig build test` all green.
2026-05-20 19:50:25 +03:00

398 lines
16 KiB
Plaintext

// Android backend driven by a `#jni_main` Activity (no native_app_glue).
//
// Lifecycle:
//
// 1. Java `SxApp.onCreate(b)` → native `sx_onCreate`: stash JNIEnv* +
// Activity globals, install the AAssetManager into the C file_utils,
// construct a `SurfaceView`, register `SxApp` as its
// SurfaceHolder.Callback, set as Activity content view.
// 2. Java `SxApp.surfaceCreated(holder)` → native `sx_surfaceCreated`:
// extract the ANativeWindow from the holder's Surface, then
// `pthread_create` the render thread on first delivery.
// 3. Render thread: brings up EGL on the ANativeWindow, then calls
// `sx_app_main()` — the user's entry-point, which sets up the
// AndroidPlatform / GPU / pipeline globals and ends in
// `run_frame_loop(closure(frame))`.
// 4. `run_frame_loop` drives the loop: drain touch events queue,
// invoke `frame_fn`, `eglSwapBuffers`, sleep ~1ms.
// 5. Java `onTouchEvent` → native `sx_onTouchEvent`: push the
// (action,x,y) tuple onto a mutex-guarded queue. `poll_events`
// drains the queue into the platform's standard `Event` shape.
//
// Vulkan-compatible: same ANativeWindow drives `vkCreate*SurfaceKHR`
// without changing the lifecycle.
#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";
// ── Foreign Java types ──────────────────────────────────────────────────
Bundle :: #foreign #jni_class("android/os/Bundle") { }
JContext :: #foreign #jni_class("android/content/Context") {
getAssets :: (self: *Self) -> *AssetManagerJ;
}
AssetManagerJ :: #foreign #jni_class("android/content/res/AssetManager") { }
Surface :: #foreign #jni_class("android/view/Surface") { }
SurfaceHolder :: #foreign #jni_class("android/view/SurfaceHolder") {
getSurface :: (self: *Self) -> *Surface;
addCallback :: (self: *Self, cb: *SurfaceHolderCallback);
}
SurfaceView :: #foreign #jni_class("android/view/SurfaceView") {
static new :: (ctx: *JContext) -> *Self;
getHolder :: (self: *Self) -> *SurfaceHolder;
}
SurfaceHolderCallback :: #foreign #jni_class("android/view/SurfaceHolder$Callback") { }
MotionEvent :: #foreign #jni_class("android/view/MotionEvent") {
getAction :: (self: *Self) -> s32;
getX :: (self: *Self) -> f32;
getY :: (self: *Self) -> f32;
}
JView :: #foreign #jni_class("android/view/View") { }
ActivityClass :: #foreign #jni_class("android/app/Activity") {
setContentView :: (self: *Self, v: *JView);
}
// ── Foreign C/NDK decls ─────────────────────────────────────────────────
// C side of file_utils — installs the AAssetManager so `read_file_bytes`
// can route through `AAssetManager_open` when running on Android.
sx_android_set_asset_manager :: (mgr: *void) #foreign;
__android_log_print :: (prio: s32, tag: *u8, fmt: *u8) -> s32 #foreign;
usleep :: (us: u32) -> s32 #foreign;
// libandroid
ANativeWindow_fromSurface :: (env: *void, surface: *void) -> *void #foreign;
ANativeWindow_release :: (window: *void) #foreign;
ANativeWindow_getWidth :: (window: *void) -> s32 #foreign;
ANativeWindow_getHeight :: (window: *void) -> s32 #foreign;
ANativeWindow_setBuffersGeometry :: (w: *void, width: s32, height: s32, fmt: s32) -> s32 #foreign;
AAssetManager_fromJava :: (env: *void, mgr: *void) -> *void #foreign;
// pthread (link libpthread is built into bionic).
pthread_create :: (thread: *u64, attr: *void, start: (*void) -> *void, arg: *void) -> s32 #foreign;
pthread_mutex_init :: (m: *void, attr: *void) -> s32 #foreign;
pthread_mutex_lock :: (m: *void) -> s32 #foreign;
pthread_mutex_unlock :: (m: *void) -> s32 #foreign;
// EGL. Constants from <EGL/egl.h>. We bring up an ES3 context with a
// 24-bit RGB framebuffer + 24-bit depth (same shape chess used under
// the legacy NDK path).
EGL_DEFAULT_DISPLAY :: 0;
EGL_NO_DISPLAY :*void: null;
EGL_NO_CONTEXT :*void: null;
EGL_NO_SURFACE :*void: null;
EGL_TRUE :u32: 1;
EGL_FALSE :u32: 0;
EGL_NONE :s32: 0x3038;
EGL_RED_SIZE :s32: 0x3024;
EGL_GREEN_SIZE :s32: 0x3023;
EGL_BLUE_SIZE :s32: 0x3022;
EGL_ALPHA_SIZE :s32: 0x3021;
EGL_DEPTH_SIZE :s32: 0x3025;
EGL_RENDERABLE_TYPE :s32: 0x3040;
EGL_SURFACE_TYPE :s32: 0x3033;
EGL_OPENGL_ES3_BIT :s32: 0x00000040;
EGL_WINDOW_BIT :s32: 0x0004;
EGL_NATIVE_VISUAL_ID :s32: 0x302E;
EGL_CONTEXT_CLIENT_VERSION :s32: 0x3098;
eglGetDisplay :: (id: u64) -> *void #foreign;
eglInitialize :: (d: *void, major: *s32, minor: *s32) -> u32 #foreign;
eglChooseConfig :: (d: *void, attrs: *s32, configs: **void, sz: s32, num: *s32) -> u32 #foreign;
eglGetConfigAttrib :: (d: *void, cfg: *void, attr: s32, value: *s32) -> u32 #foreign;
eglCreateContext :: (d: *void, cfg: *void, share: *void, attrs: *s32) -> *void #foreign;
eglCreateWindowSurface :: (d: *void, cfg: *void, window: *void, attrs: *s32) -> *void #foreign;
eglMakeCurrent :: (d: *void, draw: *void, read: *void, ctx: *void) -> u32 #foreign;
eglSwapBuffers :: (d: *void, surface: *void) -> u32 #foreign;
eglDestroyContext :: (d: *void, ctx: *void) -> u32 #foreign;
eglDestroySurface :: (d: *void, surface: *void) -> u32 #foreign;
eglTerminate :: (d: *void) -> u32 #foreign;
// ── Module-level state ──────────────────────────────────────────────────
g_activity : *void = null; // global ref to the SxApp jobject (saved env can't outlive scope)
g_app_window : *void = null; // ANativeWindow from surfaceCreated
g_egl_display : *void = null;
g_egl_context : *void = null;
g_egl_surface : *void = null;
g_egl_config : *void = null;
g_viewport_w : s32 = 0;
g_viewport_h : s32 = 0;
// Defaults to 1.0 until a proper density query lands. Chess's pipeline
// uses `viewport_w/h` as the layout space and `dpi_scale` to scale
// rendering; mismatches cause layout drift / shrinking.
g_dpi_scale : f32 = 1.0;
g_should_stop : bool = false;
g_render_thread_started : bool = false;
g_render_thread : u64 = 0;
g_frame_fn : Closure() = ---;
g_frame_fn_set : bool = false;
// Touch event queue. Single-producer (Java UI thread) / single-consumer
// (render thread); a small ring buffer guarded by a pthread mutex is
// enough — chess only generates touches on user interaction so contention
// is rare.
TouchEvent :: struct {
action: s32;
x: f32;
y: f32;
}
g_touch_queue : [64]TouchEvent = ---;
g_touch_head : u32 = 0;
g_touch_tail : u32 = 0;
// pthread_mutex_t is 40 bytes on bionic (NDK 26+); over-size to 64 for safety.
g_touch_mutex_storage : [64]u8 = ---;
g_touch_mutex_inited : bool = false;
// ── #jni_main Activity ──────────────────────────────────────────────────
// ── User-facing helpers for the consumer's `#jni_main` Activity ────────
//
// The consumer (chess, etc.) writes their own `SxApp :: #jni_main
// #jni_class("...")` declaration with `#implements SurfaceHolderCallback`
// and the standard lifecycle methods. This file provides the primitives
// those methods call:
//
// - `sx_android_forward_assets(env, activity)` from onCreate.
// - `sx_android_attach_window(env, holder)` from surfaceCreated.
// - `sx_android_detach_window()` from surfaceDestroyed.
// - `sx_android_set_viewport(w, h)` from surfaceChanged.
// - `sx_android_start_render_thread(main_fn)` once the surface is up.
// - `sx_android_push_touch(action, x, y)` from onTouchEvent.
// Extract the AAssetManager from the Activity and install it into the
// C file_utils so `read_file_bytes` can route through `AAssetManager_open`.
// Call this from your Activity's `onCreate` (BEFORE any asset read).
sx_android_forward_assets :: (env: *void, activity: *JContext) {
#jni_env(env) {
assets := activity.getAssets();
aam := AAssetManager_fromJava(env, xx assets);
sx_android_set_asset_manager(aam);
}
}
// Extract the ANativeWindow from a SurfaceHolder. Call this from your
// Activity's `surfaceCreated`. The window stays valid until
// `sx_android_detach_window` runs (typically in `surfaceDestroyed`).
sx_android_attach_window :: (env: *void, holder: *SurfaceHolder) {
#jni_env(env) {
surface := holder.getSurface();
g_app_window = ANativeWindow_fromSurface(env, xx surface);
}
}
sx_android_detach_window :: () {
if g_app_window != null {
ANativeWindow_release(g_app_window);
g_app_window = null;
}
}
sx_android_set_viewport :: (w: s32, h: s32) {
g_viewport_w = w;
g_viewport_h = h;
}
// Start the render thread that brings up EGL on `g_app_window` and calls
// the user-supplied `entry_fn` (typically the user's `main`). Safe to
// call once after `sx_android_attach_window` has set the window.
sx_android_start_render_thread :: (entry_fn: () -> void) {
if g_render_thread_started { return; }
g_user_main_fn = entry_fn;
pthread_create(@g_render_thread, null, sx_android_render_thread_entry, null);
g_render_thread_started = true;
}
g_user_main_fn : () -> void = null;
sx_android_render_thread_entry :: (arg: *void) -> *void {
while g_app_window == null and !g_should_stop {
usleep(1000);
}
if g_should_stop { return null; }
if !sx_android_egl_init() {
__android_log_print(6, "sxapp".ptr, "EGL bootstrap failed\n".ptr);
return null;
}
if g_user_main_fn != null {
g_user_main_fn();
}
null;
}
// Bring up EGL on g_app_window. Sets g_egl_display / g_egl_context /
// g_egl_surface and makes the context current. Returns false on any
// failure — caller bails on the render thread.
sx_android_egl_init :: () -> bool {
g_egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if g_egl_display == EGL_NO_DISPLAY { return false; }
major : s32 = 0;
minor : s32 = 0;
if eglInitialize(g_egl_display, @major, @minor) == EGL_FALSE { return false; }
cfg_attrs : [13]s32 = .{
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_DEPTH_SIZE, 24,
EGL_NONE,
};
num_cfg : s32 = 0;
if eglChooseConfig(g_egl_display, @cfg_attrs[0], @g_egl_config, 1, @num_cfg) == EGL_FALSE { return false; }
if num_cfg < 1 { return false; }
visual_id : s32 = 0;
eglGetConfigAttrib(g_egl_display, g_egl_config, EGL_NATIVE_VISUAL_ID, @visual_id);
ANativeWindow_setBuffersGeometry(g_app_window, 0, 0, visual_id);
ctx_attrs : [3]s32 = .{ EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
g_egl_context = eglCreateContext(g_egl_display, g_egl_config, EGL_NO_CONTEXT, @ctx_attrs[0]);
if g_egl_context == EGL_NO_CONTEXT { return false; }
g_egl_surface = eglCreateWindowSurface(g_egl_display, g_egl_config, g_app_window, null);
if g_egl_surface == EGL_NO_SURFACE { return false; }
if eglMakeCurrent(g_egl_display, g_egl_surface, g_egl_surface, g_egl_context) == EGL_FALSE { return false; }
g_viewport_w = ANativeWindow_getWidth(g_app_window);
g_viewport_h = ANativeWindow_getHeight(g_app_window);
true;
}
// ── Touch event queue ───────────────────────────────────────────────────
sx_android_push_touch :: (action: s32, x: f32, y: f32) {
sx_android_ensure_touch_mutex();
pthread_mutex_lock(xx @g_touch_mutex_storage[0]);
next := (g_touch_tail + 1) % 64;
if next != g_touch_head { // drop on full
g_touch_queue[g_touch_tail] = TouchEvent.{ action = action, x = x, y = y };
g_touch_tail = next;
}
pthread_mutex_unlock(xx @g_touch_mutex_storage[0]);
}
sx_android_drain_touches :: (out: *List(Event)) {
sx_android_ensure_touch_mutex();
pthread_mutex_lock(xx @g_touch_mutex_storage[0]);
while g_touch_head != g_touch_tail {
t := g_touch_queue[g_touch_head];
g_touch_head = (g_touch_head + 1) % 64;
// MotionEvent actions: 0=DOWN, 1=UP, 2=MOVE. Map onto chess's
// existing mouse Event variants — touch becomes a left-button
// mouse on the same screen coords; delta unused on Android.
pos : Point = .{ x = t.x, y = t.y };
if t.action == 0 {
out.append(.mouse_down(.{ position = pos, button = .left }));
} else if t.action == 1 {
out.append(.mouse_up(.{ position = pos, button = .left }));
} else if t.action == 2 {
out.append(.mouse_moved(.{ position = pos, delta = .{ x = 0, y = 0 } }));
}
}
pthread_mutex_unlock(xx @g_touch_mutex_storage[0]);
}
sx_android_ensure_touch_mutex :: () {
if g_touch_mutex_inited { return; }
pthread_mutex_init(xx @g_touch_mutex_storage[0], null);
g_touch_mutex_inited = true;
}
// ── AndroidPlatform ─────────────────────────────────────────────────────
AndroidPlatform :: struct {
title: [:0]u8 = "";
width: s32 = 0;
height: s32 = 0;
events: List(Event) = .{};
}
impl Platform for AndroidPlatform {
init :: (self: *AndroidPlatform, title: [:0]u8, w: s32, h: s32) -> bool {
self.title = title;
self.width = w;
self.height = h;
true;
}
begin_frame :: (self: *AndroidPlatform) -> FrameContext {
FrameContext.{
viewport_w = xx g_viewport_w,
viewport_h = xx g_viewport_h,
pixel_w = g_viewport_w,
pixel_h = g_viewport_h,
dpi_scale = g_dpi_scale,
delta_time = 0.016,
target_present_time = 0.0,
};
}
end_frame :: (self: *AndroidPlatform) {
if g_egl_display != null and g_egl_surface != null {
eglSwapBuffers(g_egl_display, g_egl_surface);
}
}
poll_events :: (self: *AndroidPlatform) -> []Event {
self.events.len = 0;
sx_android_drain_touches(@self.events);
result : []Event = ---;
result.ptr = self.events.items;
result.len = self.events.len;
result;
}
safe_insets :: (self: *AndroidPlatform) -> EdgeInsets {
EdgeInsets.{};
}
keyboard :: (self: *AndroidPlatform) -> KeyboardState {
KeyboardState.zero();
}
show_keyboard :: (self: *AndroidPlatform) { }
hide_keyboard :: (self: *AndroidPlatform) { }
run_frame_loop :: (self: *AndroidPlatform, frame_fn: Closure()) {
g_frame_fn = frame_fn;
g_frame_fn_set = true;
// `frame_fn` is expected to call `g_plat.end_frame()` which does
// the `eglSwapBuffers` — don't swap again here or the back buffer
// is presented twice per render, alternating with the previous
// frame's contents → visible flicker.
while !g_should_stop {
frame_fn();
usleep(1000);
}
}
stop :: (self: *AndroidPlatform) {
g_should_stop = true;
}
shutdown :: (self: *AndroidPlatform) {
if g_egl_display != null {
eglMakeCurrent(g_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if g_egl_surface != null { eglDestroySurface(g_egl_display, g_egl_surface); g_egl_surface = null; }
if g_egl_context != null { eglDestroyContext(g_egl_display, g_egl_context); g_egl_context = null; }
eglTerminate(g_egl_display);
g_egl_display = null;
}
}
}