diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08fbed3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin +lib +obj +libmain.so +.keystore diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..70a729e --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e58b88c --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +This is a basic Android native example written completely in Odin. It adapts and rewrites mmozeiko's [C example](https://github.com/mmozeiko/android-native-example) to work with Odin. + +This requires that you write some basic bindings to the Android NDK in order to compile and run. + +# Building + +First you must adjust `build.sh` and `launch.sh` to have correct paths to Android SDK, Android NDK and the Jetbrains RT. +Then set `APK` variable to the desired apk package name. Use Android SDK to install build-tools and platform SDK. + +Now you can use `build.sh` to build and install the app, and `launch.sh` to run the application: diff --git a/assets/fragment.glsl b/assets/fragment.glsl new file mode 100644 index 0000000..57f8190 --- /dev/null +++ b/assets/fragment.glsl @@ -0,0 +1,9 @@ +// + +precision mediump float; +varying vec3 color; + +void main() +{ + gl_FragColor = vec4(color, 1.0); +} diff --git a/assets/vertex.glsl b/assets/vertex.glsl new file mode 100644 index 0000000..678f8a6 --- /dev/null +++ b/assets/vertex.glsl @@ -0,0 +1,11 @@ +// + +attribute vec2 vPosition; +attribute vec3 vColor; +varying vec3 color; + +void main() +{ + color = vColor; + gl_Position = vec4(vPosition, 0.0, 1.0); +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..c701676 --- /dev/null +++ b/build.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +## TODO: handle different ABIs, currently only builds for arm64 + +APK=native_example.apk +ANDROID_SDK_ROOT=/home/illusion/Android/Sdk +ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 +ANDROID_TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64 +ANDROID_JBR=/home/illusion/android-studio/jbr +BUILD_TOOLS=34.0.0 +PLATFORM=34 + +# Create required dirs +mkdir -p lib/lib/arm64-v8a +mkdir -p bin + +# Compile the glue code that comes with the NDK to an object using the aarch64 crosscompiler. +$ANDROID_TOOLCHAIN/bin/aarch64-linux-android24-clang -c $ANDROID_NDK/sources/android/native_app_glue/android_native_app_glue.c -o android_native_app_glue.o -I $ANDROID_TOOLCHAIN/sysroot/usr/include/ + +# Create a static lib from the glue code object +$ANDROID_TOOLCHAIN/bin/llvm-ar rcs libandroid_native_app_glue.a android_native_app_glue.o + +# Compile our odin code to an object. relocation mode is set to PIC because the linker complains. +# freestanding_arm64 should probably be preferred but raylib projects need linux_arm64, otherwise the android dynamic linker fails to find our main function. +odin build jni/ -build-mode:obj -target:linux_arm64 -reloc-mode:pic || exit 1 + +# Link the static library with our object and create the shared library that will hold all our native code. +## We pass "-u ANativeActivity_onCreate" to the linker here because that symbol gets stripped by the linker even though it's required +## This is exactly what the official ndk-build building process does, see: https://github.com/android/ndk/issues/381 +$ANDROID_TOOLCHAIN/bin/aarch64-linux-android24-clang -shared -o libmain.so jni.o libandroid_native_app_glue.a -L $ANDROID_TOOLCHAIN/sysroot/usr/lib/aarch64-linux-android/$PLATFORM/ -landroid -llog -lEGL -lGLESv3 -u ANativeActivity_onCreate + +# Move the shared lib to the lib directory +cp libmain.so lib/lib/arm64-v8a + +# Pack the assets of the app using a basic android jar that comes with the SDK +## If there are asset files that need to be loaded during runtime then they should be put in an `assets` directory +## and "-A assets" should be appended after the -I option. +$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS/aapt package -f -M AndroidManifest.xml -S res -I $ANDROID_SDK_ROOT/platforms/android-$PLATFORM/android.jar -A assets -F "bin/$APK.build" lib + +# Create a debug signing key to sign our application. +if [ ! -f .keystore ]; then + "$ANDROID_JBR/bin/keytool" -genkey -dname "CN=Android Debug, O=Android, C=US" -keystore .keystore -alias androiddebugkey -storepass android -keypass android -keyalg RSA -validity 30000 + if [ $? -ne 0 ]; then exit 1; fi +fi + +# Sign the jar +$ANDROID_JBR/bin/jarsigner -storepass android -keystore .keystore "bin/$APK.build" androiddebugkey > /dev/null + +# Make it into an apk +$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS/zipalign -f 4 "bin/$APK.build" "bin/$APK" + + +cleanup() { + rm jni.o android_native_app_glue.o libandroid_native_app_glue.a bin/$APK.build +} + +cleanup diff --git a/jni/main.odin b/jni/main.odin new file mode 100644 index 0000000..2707c06 --- /dev/null +++ b/jni/main.odin @@ -0,0 +1,275 @@ +package android_native_example + +import "base:runtime" + +import "shared:android" + +import "vendor:egl" +import gl "vendor:OpenGL" + +LOG :: android.__android_log_print + +engine :: struct { + app: ^android.android_app, + + active: bool, + display: egl.Display, + surface: egl.Surface, + ctx: egl.Context, + width: i32, + height: i32, + + buffer: u32, + shader: u32, +} + +engine_init_display :: proc(engine: ^engine) { + attribs := []i32{ + egl.SURFACE_TYPE, egl.WINDOW_BIT, + egl.RENDERABLE_TYPE, egl.OPENGL_ES3_BIT, + egl.BLUE_SIZE, 8, + egl.GREEN_SIZE, 8, + egl.RED_SIZE, 8, + egl.NONE, + } + + display := egl.GetDisplay(egl.DEFAULT_DISPLAY) + if display == egl.NO_DISPLAY { + LOG(.INFO, "NativeExampleOdin", "Naterror with eglGetDisplay") + return + } + + if !egl.Initialize(display, nil, nil) { + LOG(.INFO, "NativeExampleOdin", "error with eglInitialize") + return + } + + // NOTE: I think the major and minor versions don't matter here, we just want the gl package to load all available + // gl procs. + gl.load_up_to( + 4, + 6, + proc(p: rawptr, name: cstring) {(cast(^rawptr)p)^ = egl.GetProcAddress(name)} + ) + + config: egl.Config + numConfigs: i32 + if !egl.ChooseConfig(display, raw_data(attribs), &config, 1, &numConfigs) { + LOG(.INFO, "NativeExampleOdin", "error with eglChooseConfig") + return + } + + format: i32 + if !egl.GetConfigAttrib(display, config, egl.NATIVE_VISUAL_ID, &format) { + LOG(.INFO, "NativeExampleOdin", "error with eglGetConfigAttrib") + return + } + + android.ANativeWindow_setBuffersGeometry(engine.app.window, 0, 0, format) + + surface := egl.CreateWindowSurface(display, config, cast(egl.NativeWindowType)engine.app.window, nil) + if surface == nil { + LOG(.INFO, "NativeExampleOdin", "error with eglCreateWindowSurface") + return + } + + ctx_attrib := []i32{ egl.CONTEXT_CLIENT_VERSION, 2, egl.NONE } + ctx := egl.CreateContext(display, config, nil, raw_data(ctx_attrib)) + if ctx == nil { + LOG(.INFO, "NativeExampleOdin", "error with eglCreateContext") + return + } + + if !egl.MakeCurrent(display, surface, surface, ctx) { + LOG(.INFO, "NativeExampleOdin", "error with eglMakeCurrent") + return + } + + LOG(.INFO, "NativeExampleOdin", "GL_VENDOR = %s", gl.GetString(gl.VENDOR)) + LOG(.INFO, "NativeExampleOdin", "GL_RENDERER = %s", gl.GetString(gl.RENDERER)) + LOG(.INFO, "NativeExampleOdin", "GL_VERSION = %s", gl.GetString(gl.VERSION)) + + w: i32 + h: i32 + egl.QuerySurface(display, surface, egl.WIDTH, &w) + egl.QuerySurface(display, surface, egl.HEIGHT, &h) + + LOG(.INFO, "NativeExampleOdin", "initial width: %d", w) + LOG(.INFO, "NativeExampleOdin", "initial height: %d", h) + + gl.Viewport(0, 0, w, h) + + engine.display = display + engine.ctx = ctx + engine.surface = surface + engine.width = w + engine.height = h + + vasset := android.AAssetManager_open(engine.app.activity.assetManager, "vertex.glsl", .BUFFER) + defer android.AAsset_close(vasset) + if vasset == nil { + LOG(.INFO, "NativeExampleOdin", "error opening vertex.glsl") + return + } + vsrc := cstring(android.AAsset_getBuffer(vasset)) + vlen := i32(android.AAsset_getLength(vasset)) + + v := gl.CreateShader(gl.VERTEX_SHADER) + gl.ShaderSource(v, 1, &vsrc, &vlen) + gl.CompileShader(v) + + fasset := android.AAssetManager_open(engine.app.activity.assetManager, "fragment.glsl", .BUFFER) + defer android.AAsset_close(fasset) + if fasset == nil { + LOG(.INFO, "NativeExampleOdin", "error opening fragment.glsl") + return + } + fsrc := cstring(android.AAsset_getBuffer(fasset)) + flen := i32(android.AAsset_getLength(fasset)) + + f := gl.CreateShader(gl.FRAGMENT_SHADER) + gl.ShaderSource(f, 1, &fsrc, &flen) + gl.CompileShader(f) + + + p := gl.CreateProgram() + gl.AttachShader(p, v) + gl.AttachShader(p, f) + + gl.BindAttribLocation(p, 0, "vPosition") + gl.BindAttribLocation(p, 1, "vColor") + gl.LinkProgram(p) + + gl.DeleteShader(v) + gl.DeleteShader(f) + gl.UseProgram(p) + + buf := [?]f32 { + 0.0, 0.5, 1, 0, 0, + -0.5, -0.5, 0, 1, 0, + 0.5, -0.5, 0, 0, 1, + } + + b: u32 + gl.GenBuffers(1, &b) + gl.BindBuffer(gl.ARRAY_BUFFER, b) + gl.BufferData(gl.ARRAY_BUFFER, size_of(buf), &buf, gl.STATIC_DRAW) + + engine.buffer = b + engine.shader = p + + return +} + +engine_draw_frame :: proc(engine: ^engine) { + if engine.display == nil { + return + } + + gl.ClearColor(0.258824, 0.258824, 0.435294, 1) + gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + + gl.UseProgram(engine.shader) + + gl.BindBuffer(gl.ARRAY_BUFFER, engine.buffer) + gl.VertexAttribPointer(0, 2, gl.FLOAT, gl.FALSE, (2+3)*size_of(f32), 0) + gl.VertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, (2+3)*size_of(f32), (2*size_of(f32))) + gl.EnableVertexAttribArray(0) + gl.EnableVertexAttribArray(1) + + gl.DrawArrays(gl.TRIANGLES, 0, 3) + + egl.SwapBuffers(engine.display, engine.surface) +} + +engine_term_display :: proc(engine: ^engine) { + if engine.display != egl.NO_DISPLAY { + gl.DeleteProgram(engine.shader) + gl.DeleteBuffers(1, &engine.buffer) + + egl.MakeCurrent(engine.display, egl.NO_SURFACE, egl.NO_SURFACE, egl.NO_CONTEXT) + if engine.ctx != egl.NO_CONTEXT { + egl.DestroyContext(engine.display, engine.ctx) + } + if engine.surface != egl.NO_SURFACE { + egl.DestroySurface(engine.display, engine.surface) + } + egl.Terminate(engine.display) + } + engine.active = false + engine.display = egl.NO_DISPLAY + engine.ctx = egl.NO_CONTEXT + engine.surface = egl.NO_SURFACE +} + +engine_handle_input :: proc(app: ^android.android_app, event: ^android.AInputEvent) -> i32 { + return 0 +} + +engine_handle_cmd :: proc(app: ^android.android_app, cmd: android.AppCmd) { + engine := cast(^engine)app.userData + + #partial switch (cmd) { + case .INIT_WINDOW: + if engine.app.window != nil { + LOG(.INFO, "NativeExampleOdin", "init window event") + engine_init_display(engine) + engine_draw_frame(engine) + } + + case .TERM_WINDOW: + LOG(.INFO, "NativeExampleOdin", "term window event") + engine_term_display(engine) + case .WINDOW_RESIZED: + LOG(.INFO, "NativeExampleOdin", "window resized") + h := android.ANativeWindow_getHeight(app.window) + w := android.ANativeWindow_getWidth(app.window) + LOG(.INFO, "NativeExampleOdin", "w: %d", w) + LOG(.INFO, "NativeExampleOdin", "h: %d", h) + gl.Viewport(0, 0, w, h) + + case .GAINED_FOCUS: + LOG(.INFO, "NativeExampleOdin", "gained focus event") + engine.active = true + + case .LOST_FOCUS: + LOG(.INFO, "NativeExampleOdin", "lost focus event") + engine.active = false + engine_draw_frame(engine) + } +} + +@export +android_main :: proc "contextless" (state: ^android.android_app) { + context = runtime.default_context() + + engine: engine + + state.userData = &engine + state.onAppCmd = engine_handle_cmd + state.onInputEvent = engine_handle_input + engine.app = state + + for { + events: i32 + source: ^android.android_poll_source + + ident := android.ALooper_pollAll(engine.active ? 0 : -1, nil, &events, cast(^rawptr)&source) + for ident >= 0 { + if source != nil { + source.process(state, source) + } + + if state.destroyRequested != 0 { + engine_term_display(&engine) + return + } + + ident = android.ALooper_pollAll(engine.active ? 0 : -1, nil, &events, cast(^rawptr)&source) + } + + if (engine.active) { + engine_draw_frame(&engine) + } + } +} diff --git a/launch.sh b/launch.sh new file mode 100755 index 0000000..8aa843d --- /dev/null +++ b/launch.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +APK=native_example.apk +ANDROID_SDK_ROOT=/home/illusion/Android/Sdk +ANDROID_NDK=$ANDROID_SDK_ROOT/ndk/26.1.10909125 +ANDROID_TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64 +ANDROID_JBR=/home/illusion/android-studio/jbr +BUILD_TOOLS=34.0.0 + +get_package_activity() { + PACKAGE=$("$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS/aapt" dump badging "bin/$APK" | grep package | awk -F"'" '{print $2}') + ACTIVITY=$("$ANDROID_SDK_ROOT/build-tools/$BUILD_TOOLS/aapt" dump badging "bin/$APK" | grep launchable-activity | awk -F"'" '{print $2}') +} + +launch() { + get_package_activity + "$ANDROID_SDK_ROOT/platform-tools/adb" shell am start -n "$PACKAGE/$ACTIVITY" + if [ $? -ne 0 ]; then exit 1; fi +} + +launch diff --git a/res/drawable-v24/ic_launcher_background.xml b/res/drawable-v24/ic_launcher_background.xml new file mode 100644 index 0000000..bae8060 --- /dev/null +++ b/res/drawable-v24/ic_launcher_background.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/res/drawable-v24/ic_launcher_foreground.xml b/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..7cb66ad --- /dev/null +++ b/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,10 @@ + + + diff --git a/res/mipmap-anydpi-v26/ic_launcher.xml b/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/mipmap-anydpi-v26/ic_launcher_round.xml b/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/mipmap-hdpi/ic_launcher.png b/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..cb0e65c Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher.png differ diff --git a/res/mipmap-hdpi/ic_launcher_round.png b/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..5ffd225 Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher_round.png differ