// 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); } }