From 561ad03a7cac90cb531d64084a9103e580b8decb Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 19 May 2026 00:23:33 +0300 Subject: [PATCH] android: Platform-owned entry bridge + .android OS enum variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User writes BOTH `main` and a 3-line `android_main(app)` trampoline. The library provides `sx_android_bootstrap(app)` (stashes the NDK app pointer into a platform-owned global) and `AndroidPlatform` impl of the Platform protocol. The library NEVER references `main` — the OS- shape entry symbol lives in user code where the other entry symbols already live. iOS / SDL3 keep their existing shape; only Android adds the trampoline. Cross-cutting bits this commit ships: library/modules/compiler.sx Add `android` variant to `OperatingSystem`. src/ir/lower.zig - injectComptimeConstants: map TargetConfig.isAndroid() → .android. - New Pass 4 `checkRequiredEntryPoints`: emit a clean diagnostic when `--target android` is requested but `android_main` isn't defined, instead of letting the user crash on a dlopen-time missing-symbol error. library/modules/platform/android.sx AndroidPlatform impl of the Platform protocol — EGL bringup on `APP_CMD_INIT_WINDOW`, ALooper(0) polling, dispatches the user's frame closure each ~16 ms tick. `sx_android_bootstrap(app)` is the only function exposed for the entry trampoline. examples/99-android-egl-clear.sx Rewritten to use the new pattern: minimum `main` + `android_main` pair, AndroidPlatform-driven render loop. Doubles as the usage reference users hand off to the compiler diagnostic. Verified on Pixel 7 Pro: purple clear-color frame, periodic `rendered 60 frames` logcat lines. iOS-sim chess + 86/86 regression tests pass. --- examples/99-android-egl-clear.sx | 210 ++++------------- library/modules/compiler.sx | 2 +- library/modules/platform/android.sx | 344 ++++++++++++++++++++++++++++ src/ir/lower.zig | 35 +++ 4 files changed, 426 insertions(+), 165 deletions(-) create mode 100644 library/modules/platform/android.sx diff --git a/examples/99-android-egl-clear.sx b/examples/99-android-egl-clear.sx index 01c5456..5cd059d 100644 --- a/examples/99-android-egl-clear.sx +++ b/examples/99-android-egl-clear.sx @@ -3,14 +3,22 @@ // every frame via GLES3. Equivalent of `examples/63-metal-clear.sx` // for the Android target. // -// What this exercises end-to-end (Session 70's Android port): -// - `sx build --target android` toolchain (NDK clang, native_app_glue -// compile + link, -shared .so). +// 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` and `ANativeActivity_onCreate` keep external LLVM -// linkage so the loader can call them (isExportedEntryName allowlist -// in lower.zig). -// - Foreign declarations of EGL / GLES3 / ALooper / __android_log_print. +// - `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: // @@ -21,177 +29,51 @@ // 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. `egl: setup ok` + periodic +// Expected: solid purple frame on the device. Periodic // `rendered 60 frames` lines in logcat. -// -// Known quirk: `ALooper_pollOnce(-1, ...)` (blocking variant) crashes -// inside Looper::pollOnce on Pixel 7 Pro + Android 16 with what looks -// like a stack-guard overflow. `ALooper_pollOnce(0, ...)` is fine, so -// we drive the event loop non-blocking and sleep 16ms per tick instead -// of relying on the blocking timeout. Worth revisiting later. #import "modules/std.sx"; - -// ── 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; - -// EGL — display/surface/context/config are opaque pointers from our side. -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; +#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; -// ── EGL/GL constants ─────────────────────────────────────────────────── +frame_count : s32 = 0; +g_plat : *AndroidPlatform = null; -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_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; -GL_COLOR_BUFFER_BIT :u32: 0x4000; +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(); -// ── android_app + android_poll_source layout (NDK 29, arm64) ─────────── -// Offsets measured against the NDK header on this host with offsetof(). -APP_OFF_window :s64: 72; -APP_OFF_destroyRequested :s64: 100; -SRC_OFF_process :s64: 16; // id(4) + pad(4) + app*(8) = process fn-ptr - -// ── Helpers ──────────────────────────────────────────────────────────── - -log :: (msg: *u8) { - __android_log_print(4, "sxhello".ptr, msg); -} - -read_ptr :: (base: s64, off: s64) -> *void { - p : **void = xx (base + off); - p.*; -} - -read_s32 :: (base: s64, off: s64) -> s32 { - p : *s32 = xx (base + off); - p.*; -} - -// ── EGL setup ────────────────────────────────────────────────────────── - -EGLContext :: struct { - display: *void = null; - surface: *void = null; - context: *void = null; - config: *void = null; -} - -egl_setup :: (egl: *EGLContext, window: *void) -> bool { - egl.display = eglGetDisplay(null); - if egl.display == null { log("egl: getDisplay failed\n".ptr); return false; } - - major : s32 = 0; - minor : s32 = 0; - if eglInitialize(egl.display, @major, @minor) == 0 { - log("egl: initialize failed\n".ptr); return false; + frame_count += 1; + if (frame_count % 60) == 0 { + __android_log_print(4, "sxhello".ptr, "rendered 60 frames\n".ptr); } - - attribs : [13]s32 = .{ - EGL_SURFACE_TYPE, EGL_WINDOW_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_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(egl.display, @attribs[0], @egl.config, 1, @num_config) == 0 or num_config < 1 { - log("egl: chooseConfig failed\n".ptr); return false; - } - - ctx_attribs : [3]s32 = .{ EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; - egl.context = eglCreateContext(egl.display, egl.config, null, @ctx_attribs[0]); - if egl.context == null { log("egl: createContext failed\n".ptr); return false; } - - egl.surface = eglCreateWindowSurface(egl.display, egl.config, window, null); - if egl.surface == null { log("egl: createWindowSurface failed\n".ptr); return false; } - - if eglMakeCurrent(egl.display, egl.surface, egl.surface, egl.context) == 0 { - log("egl: makeCurrent failed\n".ptr); return false; - } - - log("egl: setup ok\n".ptr); - true; } -// ── android_main ─────────────────────────────────────────────────────── -// native_app_glue invokes us on a worker thread once the activity is -// up. We poll the looper non-blocking, drain any pending sources (their -// `process` callbacks are what populate app->window for us), then -// either wait for the window or render a frame. +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) { - log("android_main: enter\n".ptr); - base : s64 = xx app; - - egl : EGLContext = .{}; - - out_fd : s32 = 0; - out_events : s32 = 0; - out_data : *void = null; - - frame : s32 = 0; - - while true { - 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(app, out_data); - } - } - } - - if read_s32(base, APP_OFF_destroyRequested) != 0 { - log("android_main: destroy requested\n".ptr); - break; - } - - window := read_ptr(base, APP_OFF_window); - if window != null and egl.surface == null { - if egl_setup(@egl, window) == false { - log("android_main: egl setup failed\n".ptr); - break; - } - } - - if egl.surface != null { - // Steady purple — same role as 63-metal-clear's blue: prove - // the GPU touched the framebuffer. - glClearColor(0.5, 0.2, 0.8, 1.0); - glClear(GL_COLOR_BUFFER_BIT); - eglSwapBuffers(egl.display, egl.surface); - frame += 1; - if (frame % 60) == 0 { - log("android_main: rendered 60 frames\n".ptr); - } - } - - usleep(16000); + inline if OS == .android { + sx_android_bootstrap(app); + main(); } } diff --git a/library/modules/compiler.sx b/library/modules/compiler.sx index 7f2f606..1209cb3 100644 --- a/library/modules/compiler.sx +++ b/library/modules/compiler.sx @@ -1,4 +1,4 @@ -OperatingSystem :: enum { macos; linux; windows; wasm; ios; unknown; } +OperatingSystem :: enum { macos; linux; windows; wasm; ios; android; unknown; } Architecture :: enum { aarch64; x86_64; wasm32; wasm64; unknown; } OS : OperatingSystem = .unknown; diff --git a/library/modules/platform/android.sx b/library/modules/platform/android.sx new file mode 100644 index 0000000..21d642a --- /dev/null +++ b/library/modules/platform/android.sx @@ -0,0 +1,344 @@ +// 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"; + +// ── 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; + +// 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; + +// 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; +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; + +// ── 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. + +sx_android_bootstrap :: (app: *void) { + g_android_app = app; +} + +// ── 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; // TODO: query AConfiguration_getDensity + 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) = .{}; + stop_requested: bool = false; + + safe_top: f32 = 0.0; + safe_left: f32 = 0.0; + safe_bottom: f32 = 0.0; + safe_right: f32 = 0.0; + + 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; + true; + } + + run_frame_loop :: (self: *AndroidPlatform, frame_fn: Closure()) { + inline if OS == .android { + self.frame_closure = frame_fn; + android_run_loop(self); + } + } + + poll_events :: (self: *AndroidPlatform) -> []Event { + out : []Event = .{ ptr = xx self.events.items, len = self.events.len }; + 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 { + 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: 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/src/ir/lower.zig b/src/ir/lower.zig index 26e9299..4172694 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -207,6 +207,39 @@ pub const Lowering = struct { self.lowerMainAndComptime(decls); // Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered self.lowerDeferredTypeFns(); + // Pass 4: target-specific entry-point sanity checks + self.checkRequiredEntryPoints(); + } + + /// On Android, the OS loader calls `android_main(app: *void)` — there's + /// no `main()` invocation from the system. If the user hasn't defined + /// `android_main`, native_app_glue can't find it at runtime and the + /// activity dies with an unhelpful "library doesn't export + /// `android_main`" error after the .so is loaded. Catch this at + /// compile time with a clear hint pointing at the platform module + /// that provides the helper. + fn checkRequiredEntryPoints(self: *Lowering) void { + const tc = self.target_config orelse return; + if (!tc.isAndroid()) return; + + const wanted = self.module.types.internString("android_main"); + var has_defn = false; + for (self.module.functions.items) |func| { + if (func.name != wanted) continue; + if (func.is_extern) continue; + if (func.blocks.items.len == 0) continue; + has_defn = true; + break; + } + if (has_defn) return; + if (self.diagnostics) |diags| { + diags.addFmt(.err, null, + "target is Android but no `android_main` function defined. " ++ + "The OS calls `android_main(app: *void)` as the entry point — " ++ + "add it to your main.sx (it can be a 3-line trampoline that " ++ + "calls `sx_android_bootstrap(app)` then `main()` — see " ++ + "`examples/99-android-egl-clear.sx`).", .{}); + } } /// Inject compile-time constants from target_config into comptime_constants. @@ -223,6 +256,8 @@ pub const Lowering = struct { self.findVariantIndex(os_info.@"enum".variants, "wasm") else if (tc.isWindows()) self.findVariantIndex(os_info.@"enum".variants, "windows") + else if (tc.isAndroid()) + self.findVariantIndex(os_info.@"enum".variants, "android") else if (tc.isLinux()) self.findVariantIndex(os_info.@"enum".variants, "linux") else if (tc.isIOS())