From a724ad772f22ed63405b7c803e39dc6325c6a37c Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Thu, 1 Aug 2024 04:36:55 +0200 Subject: [PATCH] initial commit --- .gitignore | 5 + AndroidManifest.xml | 37 +++ README.md | 10 + assets/fragment.glsl | 9 + assets/vertex.glsl | 11 + build.sh | 57 ++++ jni/main.odin | 275 ++++++++++++++++++++ launch.sh | 21 ++ res/drawable-v24/ic_launcher_background.xml | 22 ++ res/drawable-v24/ic_launcher_foreground.xml | 10 + res/mipmap-anydpi-v26/ic_launcher.xml | 5 + res/mipmap-anydpi-v26/ic_launcher_round.xml | 5 + res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 6406 bytes res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 6633 bytes 14 files changed, 467 insertions(+) create mode 100644 .gitignore create mode 100644 AndroidManifest.xml create mode 100644 README.md create mode 100644 assets/fragment.glsl create mode 100644 assets/vertex.glsl create mode 100755 build.sh create mode 100644 jni/main.odin create mode 100755 launch.sh create mode 100644 res/drawable-v24/ic_launcher_background.xml create mode 100644 res/drawable-v24/ic_launcher_foreground.xml create mode 100644 res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 res/mipmap-hdpi/ic_launcher.png create mode 100644 res/mipmap-hdpi/ic_launcher_round.png 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 0000000000000000000000000000000000000000..cb0e65cb66cc873eb512383047007651487b7b76 GIT binary patch literal 6406 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 UXPfRk8UO$Q07*qoM6N<$f~q2Rw*UYD literal 0 HcmV?d00001 diff --git a/res/mipmap-hdpi/ic_launcher_round.png b/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..5ffd2251c6e0627ad2a04535ee6ae38f61a5585b GIT binary patch literal 6633 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 literal 0 HcmV?d00001