From 29bc09c3ab90092d39da40e2f4e274cc0a7ef62f Mon Sep 17 00:00:00 2001 From: ousttrue Date: Mon, 11 Aug 2025 22:31:20 +0900 Subject: [PATCH 1/2] port native-activity from ndk-samples --- examples/native-activity/README.md | 1 + .../android/AndroidManifest.xml | 24 ++ .../android/res/mipmap/ic_launcher.png | Bin 0 -> 4812 bytes .../android/res/values/strings.xml | 10 + examples/native-activity/build.zig | 69 +++ examples/native-activity/build.zig.zon | 14 + examples/native-activity/src/c.zig | 10 + examples/native-activity/src/helper.cpp | 15 + examples/native-activity/src/main.zig | 403 ++++++++++++++++++ 9 files changed, 546 insertions(+) create mode 100644 examples/native-activity/README.md create mode 100644 examples/native-activity/android/AndroidManifest.xml create mode 100644 examples/native-activity/android/res/mipmap/ic_launcher.png create mode 100644 examples/native-activity/android/res/values/strings.xml create mode 100644 examples/native-activity/build.zig create mode 100644 examples/native-activity/build.zig.zon create mode 100644 examples/native-activity/src/c.zig create mode 100644 examples/native-activity/src/helper.cpp create mode 100644 examples/native-activity/src/main.zig diff --git a/examples/native-activity/README.md b/examples/native-activity/README.md new file mode 100644 index 0000000..f0fb3a9 --- /dev/null +++ b/examples/native-activity/README.md @@ -0,0 +1 @@ +port from https://github.com/android/ndk-samples/tree/main/native-activity 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 0000000000000000000000000000000000000000..7ea4bd87ea6b463338901e4dc0c9c496bdd46541 GIT binary patch literal 4812 zcmV;-5;N_IP)EX>4Tx04R}tkv&MmKpe$iQ>7vm2RmqS$WWauh>D1lR-p(LLaorMgUO{ILX(Ch z#l=x@EjakGSaoo5*44pP5ClI!oE)7LU8KbSC509-9vt`M-Mz=%J3y$HnPzp20-A1{ z>10C8=2pbeD?%7R7*R~i%ra&rDFxs0b&mjF@8Uem|JhiQ z+u*!U9APC{B|aw}G3kQDk6c$ge&bwpS>TxwGn1Ypju4B*Hdfl0l}wFzf;gsXI^_#l zk5$fFoV9Y5HSft^7|!diQ*eDdgG! zBgZ@{&>*{h@IUz7tyLHw^OC{|p!3CXK8AqMF3_ks&iAq7G){ovGjOH1{FOQ|^+|fI zrNxeb-fiIGx}_<5z~v6m|76If>`FnJLZJY>pV2qvfWBLxd)4c$xsTHaAVXcHZh(VB zV7N%xYd-JpYVYmeGtK^f0IAV(z0yNMy{D4^000SaNLh0L z01ejw01ejxLMWSf00007bV*G`2jmAC3_BNSOQfm*01(JYL_t(|+U;6-d{pJRe!jCz zW=S$LSuhF7%$b=i7!Z&}E+~sT2(nyTi*>Em1+}e;RP4{HwN|Y^f8Kg4SH)^^qlgIN zg3B#}8v;szNLZ6eCWHi%kjx~roH@()-am$rpojrtuWj@EH)p=(ywCUU-vRp3kAC!{ zAN}aZ4;bud54~P*eqSsu{LrNjc)eqNKA*SevtqAzgakbBZ$SWn$s))3JvD+LECjKP ze-i>&F)=z!K(V4s1ZX4}a(fOMIdY`@uSozyAqeLJ5W6%200{7Pn~Cq1j7^*9Uxom` z*L%_79|RLXw%_BQa_Gs3L!MDl6vgHD`%A&-(zSr$_jt}#6vg__uK|qRRP6DTr{t$r z01%jYBLEPg%l4TBjA5qv3lO6ILVpQk!Yy@mb?QGo0gRv%V^08J4A_z)f=y8rWe~t( zJ+xy6z@`JRGm$?4hL+>qOZ=e-ttg5T4u``4kmYiY2G|vDr;C#)e833T0}vP}k1;d= z7zF^5$jU@fuuB56IAJh|D8uPo5s5_9-n*C|O1xK;XP6|-tFEr@2q?;A5Z(pA8L`>K z=sp00!I)l_FYmczjKei`RC+;n`jO}NReiE{(VC9xkE_%#^mgmRRV`J~?}dOsVW1cS zKiid?wP@3(O?n?CJbwmQVNs~=$7=x!c$Mt{N&%RR(e42ml56qp`B?+Aq_po^t2tse z&so^KetUy@e|0Fdxz9|%=kpC4>dXD(<>S+@9#vu;{>j$J*7lC}#=b~(-Og;0F?yiG z$dLsIRJi=A@6cQmX}pmS8IN+Qo(@X01^N_2ViV@ zQJTE`&Vr#QvNjQD)?@qLs@kXjo4Sn&F;B2=_1 z_;{y|Vs{h{s{qEphIp*v>z_qi+9Kh~6A6Q?z$q_yZSKG+Wd*XIn+E2+c;L(GeKao^ z3`Y9G_LGM3CK#O=ZaugXvfMxt*7)DLJX~_w&(PM^hDD1O zAwNGKRkbbH)70&opS&*jg4Mq*yyos%?)!%n`{(qEfD*6w)XOJ2C!glElyJ_`8IICZ ztK;tlL!o#2D)h-ryw2zIl?CgAZxiTx0NG&lOi`9RdCp|#s9vohn25y_aJgI#rU}z9K&FX`2E;zusjul75speghJ$`|)r8EE0AM7O*c*oVWzbmO zm!~g69981;CHK_U{<_$s=)^Er7Lh%FrrYuLv2`sv>=uUY4H_;SZpS@KTCwc&D5j4} z$D>!~AdpL#o4XrEGKsgpj5fc#mM6;x3Yj1X%^j53almNbzR#$v+Gi}TsjFMrbM2!N zP+lkxvq&rr08CxS>K#UNlJmFvNOl1L_tw|0+3Qud7JHNrOpz_J3K(_YrP-s!ui5Tv z>!Pq*8BQH&fh+vC8O42q|yk?IbTZgNVl-l?^% z+s@Cg-dEkwt1wtvSU7M@sV(QQ`IsC#8_i%S6l&@V>5IMIVF9mq%38o6E?i9vHwc`z zp1_sg=P?KzqF(>r0@I>m{7 z`(vnYO5m#?!KgEE=KvAHA=hzGC93E&R#jE)(N3r%5JSTke zigKP#R4EW?F{}L!FuIG)fU*(Mzg=1HvlLp)I#Hx09azxCV=ER$yw_=o6LbTSv z;5-R*sGeWaXjC0gSvVN1*?rP&anoQmjcumEd3TwA@u-Y}sSAl?)b)P9^HBNhQWT|( zbACYJ^vIr2Xa_)U(#^$^$m(ynz&Y^M+w;%*t}3q!w@!9p#l|Q;+}w^SLu4#nt7CR< z>fAnUHpBbloHuK<<1bsPZ|{?P0WM-|Rg>9asNJRhl@rs)o?2kJEf5F{_=eyf|1}duHJ5AU` zK;wiBowVz*TJwpjPQLD>9MlnYKp@a&W65jI^syYtng+&F?se9fbl=9^@p6^YlkJhn zZUE%-`GyIg-?T@b;Q3W5uE`N0FopwhV11|Y{(_2{TedZ~uRp1N+}Cl@n3BbdZ?Ih^ zTdlA-j(q2jw2F?NA0!s#1eskEmt<^17x>+G=^WezKI zeB0)cILCtb+7VYduA1aRX+d`dN2Uq2GYW2|BGR&T1_rlql&3Rj9H(yc7LD}wcA z<5-gLyCtodKROMWGQ&e3NASSxR03)wHE6LI=ulT@cN0OcHBg?HQrMXeoVi(VEZLCU1pvv;D9ALAIyp^dR+mF?^$@<-Sg@)MHxH7KlLlA>f@l)7qy{z8 ziDWc-Sonr^hWA#hxa^SdZ7Ly>1dzcPBN}(|Rq0XC)YQaPo?)F-o_4OwMxvo<5F{DG zp=>p&6V_BE@Ya_xd{Lu9GYFm>8LXE*=7h$vW0YWqWND9xI_|uXjkcgo;qNrfsPstXiew zwtNvHV~FX5P?V!qBaA4q!0i;|bj#5jCnxO#0HCU>N>e*MzxvB(mgU!vkeo8?j8qg3W>@x_N`2t4xEUC0zV%G1)2XvvPzJAl9k79SP{K{-r zb;8FxOx!dyC46;{Q9HxCNYDZao~k3_$;02>^hqU}*6}E91BRW`)*t zp07Jh6=zDwvkAC+Z47%Gb%ggDpk98X;j}We>42#pd8u?!frQi381m8>+LMGm5sv*T z$Lg@T^{uG6essPBN$w_1)yeqYOrR%r)z-dd8s3uG#Tj=4IPCE!`79{3F#NeHi5Z15 zlv9q*XHo-p?kDWnYa-HaLXQ&mCpe}T9l8Cq)2x`cyaT5Q5_AsiN$~LMu>N#~;ka*B ze)2P`qkH{>ao*OEMEyU6007_uc6KId7l4sH(Zxg1_?`}MfN>74 z0Z0(iOu`_CfQuXqPMZMOfcd*kBvlS>0)p5TRsmPKfX_P2XswDj-fz+G-4zPe2E5*L zE>_Zu($c#L_&8+LFpiD?Fak8wT>Da`ddsv@%aQghFv~q0A>8Q87PES) z|7$zc4lc8gzIPvRxSiF|(6+7B{QJ8#`lhe{XOS5o1FAvj&0Dv}Y@(pWO@6S+!2YHLsy0N-PXvbHU$mMJj&}%|s?5S9Fyqpt9c@1!k`~ywmpuyL zLNGpd<9=E&X`tn%$uCDXK6b7>XF-KpZ*u-L`T)m8KtNGu*kl^=&UQ^}iSo@C_$*}( zE3>OAQDf9x|K{%6YmamP&z{)kpLv=~Jm>PY@y7fA!ruqzhb4k%V;l18-h}e12BztI8P;W>kV4d(nq%{^d zwbz1LQ&^+tPGEmt+ex=S`c-n5YVgXtK8nAWFsUpU3N5a#s~gD~+mX80L>STdr0Uw* z57zB7LZ&GH0smj{`Ful56y-pP&wFRU>w9qFk4_B^E%q-snKk{2@-x3jc?f_Q@F + + + 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); +} From 679ef008ee2b564d1a4c676ffbe273b4605c0ef5 Mon Sep 17 00:00:00 2001 From: ousttrue Date: Mon, 11 Aug 2025 22:40:40 +0900 Subject: [PATCH 2/2] update README --- examples/native-activity/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/examples/native-activity/README.md b/examples/native-activity/README.md index f0fb3a9..759286d 100644 --- a/examples/native-activity/README.md +++ b/examples/native-activity/README.md @@ -1 +1,17 @@ -port from https://github.com/android/ndk-samples/tree/main/native-activity +# 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 +```