android: Platform-owned entry bridge + .android OS enum variant
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.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user