diff --git a/examples/99-android-egl-clear.sx b/examples/99-android-egl-clear.sx new file mode 100644 index 0000000..01c5456 --- /dev/null +++ b/examples/99-android-egl-clear.sx @@ -0,0 +1,197 @@ +// 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. +// +// 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). +// - `--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. +// +// 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. `egl: setup ok` + 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; + +// GLES3 (linked via -lGLESv3) +glClearColor :: (r: f32, g: f32, b: f32, a: f32) #foreign; +glClear :: (mask: u32) #foreign; + +// ── EGL/GL 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_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; + +// ── 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; + } + + 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. + +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); + } +}