diff --git a/examples/native-activity/README.md b/examples/native-activity/README.md new file mode 100644 index 0000000..759286d --- /dev/null +++ b/examples/native-activity/README.md @@ -0,0 +1,17 @@ +# NativeActivity + +Ported from https://github.com/android/ndk-samples/tree/main/native-activity . + +This example demonstrates the use of native_app_glue and android_main. + +## std.log override + +```zig +_ = c.__android_log_write(priority, "ZIG", &buf.buffer); +``` + +You can display the logs filtered by "ZIG" in color by following the steps below. + +``` +$ adb logcat -s "ZIG" -v color +``` diff --git a/examples/native-activity/android/AndroidManifest.xml b/examples/native-activity/android/AndroidManifest.xml new file mode 100644 index 0000000..1813be5 --- /dev/null +++ b/examples/native-activity/android/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/examples/native-activity/android/res/mipmap/ic_launcher.png b/examples/native-activity/android/res/mipmap/ic_launcher.png new file mode 100644 index 0000000..7ea4bd8 Binary files /dev/null and b/examples/native-activity/android/res/mipmap/ic_launcher.png differ diff --git a/examples/native-activity/android/res/values/strings.xml b/examples/native-activity/android/res/values/strings.xml new file mode 100644 index 0000000..0080f5e --- /dev/null +++ b/examples/native-activity/android/res/values/strings.xml @@ -0,0 +1,10 @@ + + + + Zig NativeActivity + + com.zig.native_activity + diff --git a/examples/native-activity/build.zig b/examples/native-activity/build.zig new file mode 100644 index 0000000..288c003 --- /dev/null +++ b/examples/native-activity/build.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const android = @import("android"); + +pub fn build(b: *std.Build) void { + + // const target = b.standardTargetOptions(.{}); + const target = b.resolveTargetQuery(.{ + .cpu_arch = .aarch64, + .os_tag = .linux, + .abi = .android, + }); + const optimize = b.standardOptimizeOption(.{}); + + const android_sdk = android.Sdk.create(b, .{}); + const apk = android_sdk.createApk(.{ + .api_level = .android15, + .build_tools_version = "35.0.1", + .ndk_version = "29.0.13113456", + }); + const key_store_file = android_sdk.createKeyStore(.example); + apk.setKeyStore(key_store_file); + apk.setAndroidManifest(b.path("android/AndroidManifest.xml")); + apk.addResourceDirectory(b.path("android/res")); + + const exe = b.addSharedLibrary(.{ + .name = "native-activity", + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/main.zig"), + .link_libc = true, + }); + b.installArtifact(exe); + + exe.addIncludePath(.{ .cwd_relative = b.fmt("{s}/sources/android/native_app_glue", .{apk.ndk.path}) }); + exe.addCSourceFile(.{ + .file = .{ .cwd_relative = b.fmt("{s}/sources/android/native_app_glue/android_native_app_glue.c", .{apk.ndk.path}) }, + }); + exe.addCSourceFile(.{ + .file = b.path("src/helper.cpp"), + }); + // exe.target_link_options(${TARGET_NAME} PUBLIC -u ANativeActivity_onCreate) + const libs = [_][]const u8{ + "android", + "EGL", + "GLESv1_CM", + "log", + }; + for (libs) |lib| { + exe.linkSystemLibrary(lib); + } + + const android_dep = b.dependency("android", .{ + .optimize = optimize, + .target = target, + }); + exe.root_module.addImport("android", android_dep.module("android")); + + apk.addArtifact(exe); + + const installed_apk = apk.addInstallApk(); + b.getInstallStep().dependOn(&installed_apk.step); + + const run_step = b.step("run", "Install and run the application on an Android device"); + const adb_install = android_sdk.addAdbInstall(installed_apk.source); + const adb_start = android_sdk.addAdbStart("com.zig.native_activity/android.app.NativeActivity"); + adb_start.step.dependOn(&adb_install.step); + run_step.dependOn(&adb_start.step); +} diff --git a/examples/native-activity/build.zig.zon b/examples/native-activity/build.zig.zon new file mode 100644 index 0000000..bf85354 --- /dev/null +++ b/examples/native-activity/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = .native_activity, + .version = "0.0.0", + .fingerprint = 0xe31e7fb5499884f3, // Changing this has security and trust implications. + .minimum_zig_version = "0.14.1", + .dependencies = .{ + .android = .{ .path = "../.." }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/examples/native-activity/src/c.zig b/examples/native-activity/src/c.zig new file mode 100644 index 0000000..d39fd6d --- /dev/null +++ b/examples/native-activity/src/c.zig @@ -0,0 +1,10 @@ +pub usingnamespace @cImport({ + @cInclude("EGL/egl.h"); + @cInclude("GLES/gl.h"); + @cInclude("android/choreographer.h"); + @cInclude("android/log.h"); + @cInclude("android/sensor.h"); + @cInclude("android/set_abort_message.h"); + @cInclude("android_native_app_glue.h"); + // #include +}); diff --git a/examples/native-activity/src/helper.cpp b/examples/native-activity/src/helper.cpp new file mode 100644 index 0000000..be26c58 --- /dev/null +++ b/examples/native-activity/src/helper.cpp @@ -0,0 +1,15 @@ +#include +#include + +extern "C" { + +void call_souce_process(android_app *state, android_poll_source *s) { + s->process(state, s); +} + +const float* get_acceleration(const ASensorEvent *event) +{ + return &event->acceleration.x; +} + +} diff --git a/examples/native-activity/src/main.zig b/examples/native-activity/src/main.zig new file mode 100644 index 0000000..e6d1aa5 --- /dev/null +++ b/examples/native-activity/src/main.zig @@ -0,0 +1,403 @@ +const std = @import("std"); +const c = @import("c.zig"); + +extern fn call_souce_process(state: *c.android_app, s: *c.android_poll_source) void; +extern fn get_acceleration(event: *const c.ASensorEvent) [*]const f32; + +// https://ziggit.dev/t/set-debug-level-at-runtime/6196/3 +pub const std_options: std.Options = .{ + .logFn = logFn, + .log_level = .debug, +}; + +fn CHECK_NOT_NULL(p: ?*const anyopaque) void { + if (p == null) { + @panic("null !"); + } +} + +// https://github.com/vamolessa/zig-sdl-android-template/blob/master/src/android_main.zig +// make the std.log. functions write to the android log +pub fn logFn( + comptime message_level: std.log.Level, + comptime scope: @Type(.enum_literal), + comptime format: []const u8, + args: anytype, +) void { + const priority = switch (message_level) { + .err => c.ANDROID_LOG_ERROR, + .warn => c.ANDROID_LOG_WARN, + .info => c.ANDROID_LOG_INFO, + .debug => c.ANDROID_LOG_DEBUG, + }; + const prefix = if (scope == .default) "" else "(" ++ @tagName(scope) ++ "): "; + + var buf = std.io.FixedBufferStream([4 * 1024]u8){ + .buffer = undefined, + .pos = 0, + }; + var writer = buf.writer(); + writer.print(prefix ++ format, args) catch {}; + + if (buf.pos >= buf.buffer.len) { + buf.pos = buf.buffer.len - 1; + } + buf.buffer[buf.pos] = 0; + + _ = c.__android_log_write(priority, "ZIG", &buf.buffer); +} + +// for log message @tagName +const AppCmd = enum(c_int) { + APP_CMD_INPUT_CHANGED = c.APP_CMD_INPUT_CHANGED, + APP_CMD_INIT_WINDOW = c.APP_CMD_INIT_WINDOW, + APP_CMD_TERM_WINDOW = c.APP_CMD_TERM_WINDOW, + APP_CMD_WINDOW_RESIZED = c.APP_CMD_WINDOW_RESIZED, + APP_CMD_WINDOW_REDRAW_NEEDED = c.APP_CMD_WINDOW_REDRAW_NEEDED, + APP_CMD_CONTENT_RECT_CHANGED = c.APP_CMD_CONTENT_RECT_CHANGED, + APP_CMD_GAINED_FOCUS = c.APP_CMD_GAINED_FOCUS, + APP_CMD_LOST_FOCUS = c.APP_CMD_LOST_FOCUS, + APP_CMD_CONFIG_CHANGED = c.APP_CMD_CONFIG_CHANGED, + APP_CMD_LOW_MEMORY = c.APP_CMD_LOW_MEMORY, + APP_CMD_START = c.APP_CMD_START, + APP_CMD_RESUME = c.APP_CMD_RESUME, + APP_CMD_SAVE_STATE = c.APP_CMD_SAVE_STATE, + APP_CMD_PAUSE = c.APP_CMD_PAUSE, + APP_CMD_STOP = c.APP_CMD_STOP, + APP_CMD_DESTROY = c.APP_CMD_DESTROY, +}; + +const SavedState = struct { + angle: f32 = 0, + x: i32 = 0, + y: i32 = 0, +}; + +const Engine = struct { + app: *c.android_app, + + sensorManager: ?*c.ASensorManager = null, + accelerometerSensor: ?*const c.ASensor = null, + sensorEventQueue: ?*c.ASensorEventQueue = null, + + display: c.EGLDisplay = null, + surface: c.EGLSurface = null, + context: c.EGLContext = null, + width: i32 = 0, + height: i32 = 0, + state: SavedState = .{}, + + running_: bool = false, + + fn CreateSensorListener(self: *@This(), callback: c.ALooper_callbackFunc) void { + std.log.info(" Engine.CreateSensorListener()", .{}); + CHECK_NOT_NULL(self.app); + + self.sensorManager = c.ASensorManager_getInstance(); + if (self.sensorManager == null) { + return; + } + + self.accelerometerSensor = c.ASensorManager_getDefaultSensor( + self.sensorManager, + c.ASENSOR_TYPE_ACCELEROMETER, + ); + self.sensorEventQueue = c.ASensorManager_createEventQueue( + self.sensorManager, + self.app.looper, + c.ALOOPER_POLL_CALLBACK, + callback, + self, + ); + } + + /// Resumes ticking the application. + fn Resume(self: *@This()) void { + std.log.info(" Engine.Resume()", .{}); + // Checked to make sure we don't double schedule Choreographer. + if (!self.running_) { + std.log.info(" start tick", .{}); + self.running_ = true; + self.ScheduleNextTick(); + } + } + + fn Pause(self: *@This()) void { + std.log.info(" Engine.Pause()", .{}); + self.running_ = false; + } + + fn ScheduleNextTick(self: *@This()) void { + c.AChoreographer_postFrameCallback(c.AChoreographer_getInstance(), &Tick, self); + } + + fn Tick(_: c_long, data: ?*anyopaque) callconv(.C) void { + CHECK_NOT_NULL(data); + const engine: *Engine = @ptrCast(@alignCast(data)); + engine.DoTick(); + } + + fn DoTick(self: *@This()) void { + if (!self.running_) { + return; + } + + // Input and sensor feedback is handled via their own callbacks. + // Choreographer ensures that those callbacks run before this callback does. + + // Choreographer does not continuously schedule the callback. We have to re- + // register the callback each time we're ticked. + self.ScheduleNextTick(); + self.Update(); + self.DrawFrame(); + } + + fn Update(self: *@This()) void { + self.state.angle += 0.01; + if (self.state.angle > 1) { + self.state.angle = 0; + } + } + + fn DrawFrame(self: *@This()) void { + if (self.display == null) { + // No display. + return; + } + + // Just fill the screen with a color. + c.glClearColor( + @as(f32, @floatFromInt(self.state.x)) / @as(f32, @floatFromInt(self.width)), + self.state.angle, + @as(f32, @floatFromInt(self.state.y)) / @as(f32, @floatFromInt(self.height)), + 1, + ); + c.glClear(c.GL_COLOR_BUFFER_BIT); + _ = c.eglSwapBuffers(self.display, self.surface); + } +}; + +fn getEglConfig(display: c.EGLDisplay) ?c.EGLConfig { + const attribs = [_]c.EGLint{ + c.EGL_SURFACE_TYPE, + c.EGL_WINDOW_BIT, + c.EGL_BLUE_SIZE, + 8, + c.EGL_GREEN_SIZE, + 8, + c.EGL_RED_SIZE, + 8, + c.EGL_NONE, + }; + var numConfigs: c.EGLint = undefined; + if (c.eglChooseConfig(display, &attribs, null, 0, &numConfigs) != c.EGL_TRUE) { + return null; + } + if (numConfigs == 0) { + std.log.err(" zero config", .{}); + return null; + } + + const supportedConfigs = std.heap.page_allocator.alloc(c.EGLConfig, @intCast(numConfigs)) catch @panic("OOP"); + defer std.heap.page_allocator.free(supportedConfigs); + if (c.eglChooseConfig(display, &attribs, &supportedConfigs[0], numConfigs, &numConfigs) != c.EGL_TRUE) { + return null; + } + + for (supportedConfigs) |cfg| { + var r: c.EGLint = undefined; + var g: c.EGLint = undefined; + var b: c.EGLint = undefined; + var d: c.EGLint = undefined; + if (c.eglGetConfigAttrib(display, cfg, c.EGL_RED_SIZE, &r) != 0 and + c.eglGetConfigAttrib(display, cfg, c.EGL_GREEN_SIZE, &g) != 0 and + c.eglGetConfigAttrib(display, cfg, c.EGL_BLUE_SIZE, &b) != 0 and + c.eglGetConfigAttrib(display, cfg, c.EGL_DEPTH_SIZE, &d) != 0 and r == 8 and + g == 8 and b == 8 and d == 0) + { + return cfg; + } + } + std.log.warn(" config not found. use first.", .{}); + return supportedConfigs[0]; +} + +fn engine_init_display(engine: *Engine, window: *c.ANativeWindow) void { + // initialize OpenGL ES and EGL + std.log.info(" engine_init_display", .{}); + + const display = c.eglGetDisplay(c.EGL_DEFAULT_DISPLAY); + std.log.debug(" display: {?}", .{display}); + std.debug.assert(c.EGL_TRUE == c.eglInitialize(display, null, null)); + + const config = getEglConfig(display) orelse @panic("Unable to initialize EGLConfig"); + + var format: c.EGLint = undefined; + _ = c.eglGetConfigAttrib(display, config, c.EGL_NATIVE_VISUAL_ID, &format); + const surface = c.eglCreateWindowSurface(display, config, window, null); + const context = c.eglCreateContext(display, config, null, null); + if (c.eglMakeCurrent(display, surface, surface, context) == c.EGL_FALSE) { + @panic("Unable to eglMakeCurrent"); + } + + var w: c.EGLint = undefined; + _ = c.eglQuerySurface(display, surface, c.EGL_WIDTH, &w); + var h: c.EGLint = undefined; + _ = c.eglQuerySurface(display, surface, c.EGL_HEIGHT, &h); + + engine.display = display; + engine.context = context; + engine.surface = surface; + engine.width = w; + engine.height = h; + engine.state.angle = 0; + + // Check openGL on the system + const opengl_info = [4]c.GLenum{ c.GL_VENDOR, c.GL_RENDERER, c.GL_VERSION, c.GL_EXTENSIONS }; + for (opengl_info) |name| { + const info = c.glGetString(name); + std.log.info("OpenGL Info: {s}", .{info}); + } + // Initialize GL state. + c.glHint(c.GL_PERSPECTIVE_CORRECTION_HINT, c.GL_FASTEST); + c.glEnable(c.GL_CULL_FACE); + c.glShadeModel(c.GL_SMOOTH); + c.glDisable(c.GL_DEPTH_TEST); +} + +fn engine_term_display(engine: *Engine) void { + std.log.info(" engine_term_display", .{}); + if (engine.display != c.EGL_NO_DISPLAY) { + _ = c.eglMakeCurrent(engine.display, c.EGL_NO_SURFACE, c.EGL_NO_SURFACE, c.EGL_NO_CONTEXT); + if (engine.context != c.EGL_NO_CONTEXT) { + _ = c.eglDestroyContext(engine.display, engine.context); + } + if (engine.surface != c.EGL_NO_SURFACE) { + _ = c.eglDestroySurface(engine.display, engine.surface); + } + _ = c.eglTerminate(engine.display); + } + engine.Pause(); + engine.display = c.EGL_NO_DISPLAY; + engine.context = c.EGL_NO_CONTEXT; + engine.surface = c.EGL_NO_SURFACE; +} + +fn engine_handle_input(app: [*c]c.android_app, event: ?*c.AInputEvent) callconv(.C) i32 { + const t = c.AInputEvent_getType(event); + std.log.debug("engine_handle_input: event = {}", .{t}); + var engine: *Engine = @ptrCast(@alignCast(app[0].userData)); + if (t == c.AINPUT_EVENT_TYPE_MOTION) { + engine.state.x = @intFromFloat(c.AMotionEvent_getX(event, 0)); + engine.state.y = @intFromFloat(c.AMotionEvent_getY(event, 0)); + return 1; + } + return 0; +} + +fn engine_handle_cmd(app: [*c]c.android_app, _cmd: i32) callconv(.C) void { + const cmd: AppCmd = @enumFromInt(_cmd); + std.log.debug("engine_handle_cmd: cmd = {s}", .{@tagName(cmd)}); + const engine: *Engine = @ptrCast(@alignCast(app[0].userData)); + switch (cmd) { + .APP_CMD_SAVE_STATE => { + // The system has asked us to save our current state. Do so. + engine.app.savedState = std.heap.page_allocator.create(SavedState) catch @panic("OOP"); + @as(*SavedState, @ptrCast(@alignCast(engine.app.savedState))).* = engine.state; + engine.app.savedStateSize = @sizeOf(SavedState); + }, + .APP_CMD_INIT_WINDOW => { + // The window is being shown, get it ready. + if (engine.app.window) |window| { + engine_init_display(engine, window); + } + }, + .APP_CMD_TERM_WINDOW => { + // The window is being hidden or closed, clean it up. + engine_term_display(engine); + }, + .APP_CMD_GAINED_FOCUS => { + // When our app gains focus, we start monitoring the accelerometer. + if (engine.accelerometerSensor != null) { + _ = c.ASensorEventQueue_enableSensor(engine.sensorEventQueue, engine.accelerometerSensor); + // We'd like to get 60 events per second (in us). + _ = c.ASensorEventQueue_setEventRate(engine.sensorEventQueue, engine.accelerometerSensor, (1000 / 60) * 1000); + } + engine.Resume(); + }, + .APP_CMD_LOST_FOCUS => { + // When our app loses focus, we stop monitoring the accelerometer. + // This is to avoid consuming battery while not being used. + if (engine.accelerometerSensor != null) { + _ = c.ASensorEventQueue_disableSensor(engine.sensorEventQueue, engine.accelerometerSensor); + } + engine.Pause(); + }, + else => {}, + } +} + +fn OnSensorEvent(fd: c_int, events: c_int, data: ?*anyopaque) callconv(.C) i32 { + _ = fd; + _ = events; + + CHECK_NOT_NULL(data); + const engine: *Engine = @ptrCast(@alignCast(data)); + + CHECK_NOT_NULL(engine.accelerometerSensor); + var event: c.ASensorEvent = undefined; + while (c.ASensorEventQueue_getEvents(engine.sensorEventQueue, &event, 1) > 0) { + // extern union ? + // const acceleration: [*]const f32 = get_acceleration(&event); + // std.log.debug( + // "accelerometer: x={} y={} z={}", + // .{ + // acceleration[0], + // acceleration[1], + // acceleration[2], + // }, + // ); + } + + // From the docs: + + // Implementations should return 1 to continue receiving callbacks, or 0 to + // have this file descriptor and callback unregistered from the looper. + return 1; +} + +export fn android_main(state: *c.android_app) callconv(.C) void { + std.log.info("#### android_main ####", .{}); + + var engine = Engine{ + .app = state, + }; + + state.userData = &engine; + state.onAppCmd = &engine_handle_cmd; + state.onInputEvent = &engine_handle_input; + + // Prepare to monitor accelerometer + engine.CreateSensorListener(&OnSensorEvent); + + if (state.savedState != null) { + // We are starting with a previous saved state; restore from it. + engine.state = @as(*SavedState, @ptrCast(@alignCast(state.savedState.?))).*; + } + + while (state.destroyRequested == 0) { + // Our input, sensor, and update/render logic is all driven by callbacks, so + // we don't need to use the non-blocking poll. + var source: ?*c.android_poll_source = null; + const result = c.ALooper_pollOnce(-1, null, null, @ptrCast(&source)); + if (result == c.ALOOPER_POLL_ERROR) { + @panic("ALooper_pollOnce returned an error"); + } + + if (source) |s| { + call_souce_process(state, s); + } + } + + engine_term_display(&engine); +}