From 5b087ae04cf8a2d124bbb2b62056b33c625dceff Mon Sep 17 00:00:00 2001 From: Nikolay Igotti Date: Thu, 24 May 2018 18:20:59 +0300 Subject: [PATCH] Improve Android native sample, add basic Java bridge. (#1606) --- .../jetbrains/kotlin/backend/konan/ir/Ir.kt | 2 +- .../kotlin/backend/konan/llvm/IrToBitcode.kt | 14 +- .../konan/lower/CallableReferenceLowering.kt | 2 +- .../lower/DefaultArgumentsStubGenerator.kt | 2 +- .../konan/lower/ExpectDeclarationsRemoving.kt | 2 +- .../backend/konan/optimizations/DFGBuilder.kt | 2 +- runtime/src/launcher/cpp/androidLauncher.cpp | 1 - samples/androidNativeActivity/README.md | 6 +- samples/androidNativeActivity/android.def | 8 - .../androidNativeActivity/androidLauncher.h | 85 -------- samples/androidNativeActivity/build.gradle | 2 +- .../src/main/AndroidManifest.xml | 2 + .../src/main/kotlin/BMPHeader.kt | 33 +++ .../src/main/kotlin/Disposable.kt | 73 +++++++ .../src/main/kotlin/{engine.kt => Engine.kt} | 122 ++++++----- .../src/main/kotlin/JniBridge.kt | 118 +++++++++++ .../kotlin/{polyhedra.kt => Polyhedra.kt} | 3 +- .../main/kotlin/{renderer.kt => Renderer.kt} | 192 +++++++++--------- .../main/kotlin/{vectors.kt => Vectors.kt} | 3 +- .../src/main/kotlin/main.kt | 34 ++++ 20 files changed, 445 insertions(+), 261 deletions(-) delete mode 100644 samples/androidNativeActivity/android.def delete mode 100644 samples/androidNativeActivity/androidLauncher.h create mode 100644 samples/androidNativeActivity/src/main/kotlin/BMPHeader.kt create mode 100644 samples/androidNativeActivity/src/main/kotlin/Disposable.kt rename samples/androidNativeActivity/src/main/kotlin/{engine.kt => Engine.kt} (65%) create mode 100644 samples/androidNativeActivity/src/main/kotlin/JniBridge.kt rename samples/androidNativeActivity/src/main/kotlin/{polyhedra.kt => Polyhedra.kt} (98%) rename samples/androidNativeActivity/src/main/kotlin/{renderer.kt => Renderer.kt} (62%) rename samples/androidNativeActivity/src/main/kotlin/{vectors.kt => Vectors.kt} (96%) create mode 100644 samples/androidNativeActivity/src/main/kotlin/main.kt diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt index e9dfad955a0..4462a9fbdb3 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/ir/Ir.kt @@ -57,7 +57,7 @@ internal class KonanIr(context: Context, irModule: IrModuleFragment): Ir, resultLifetime: Lifetime): LLVMValueRef { - val descriptor = callee.symbol.owner as IrFunction + val descriptor = callee.symbol.owner val argsWithContinuationIfNeeded = if (descriptor.isSuspend) args + getContinuation() @@ -2021,7 +2021,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map): LLVMValueRef { context.log{"evaluateOperatorCall : origin:${ir2string(callee)}"} - val descriptor = callee.symbol.owner as IrFunction + val descriptor = callee.symbol.owner val ib = context.irModule!!.irBuiltins with(functionGenerationContext) { diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/CallableReferenceLowering.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/CallableReferenceLowering.kt index 4b8012063ca..0697327adf5 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/CallableReferenceLowering.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/CallableReferenceLowering.kt @@ -305,7 +305,7 @@ internal class CallableReferenceLowering(val context: Context): FileLoweringPass IrConstKind.String, functionDescriptor.name.asString()) putValueArgument(0, name) val fqName = IrConstImpl(startOffset, endOffset, context.builtIns.stringType, IrConstKind.String, - (functionReference.symbol.owner as IrFunction).fullName) + (functionReference.symbol.owner).fullName) putValueArgument(1, fqName) val bound = IrConstImpl.boolean(startOffset, endOffset, context.builtIns.booleanType, boundFunctionParameters.isNotEmpty()) diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/DefaultArgumentsStubGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/DefaultArgumentsStubGenerator.kt index c337dd1c288..a8154ad474a 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/DefaultArgumentsStubGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/DefaultArgumentsStubGenerator.kt @@ -295,7 +295,7 @@ class DefaultParameterInjector constructor(val context: CommonBackendContext): B private fun parametersForCall(expression: IrFunctionAccessExpression): Pair>> { val descriptor = expression.descriptor - val keyFunction = (expression.symbol.owner as IrFunction).findSuperMethodWithDefaultArguments()!! + val keyFunction = expression.symbol.owner.findSuperMethodWithDefaultArguments()!! val keyDescriptor = keyFunction.descriptor val realFunction = keyDescriptor.generateDefaultsFunction(context) realFunction.parent = keyFunction.parent diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/ExpectDeclarationsRemoving.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/ExpectDeclarationsRemoving.kt index def7687e371..7468b3269d8 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/ExpectDeclarationsRemoving.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/lower/ExpectDeclarationsRemoving.kt @@ -61,7 +61,7 @@ internal class ExpectDeclarationsRemoving(val context: Context) : FileLoweringPa } private fun IrFunction.findActualForExpected(): IrFunction = - context.ir.symbols.symbolTable.referenceFunction(descriptor.findActualForExpect()).owner as IrFunction + context.ir.symbols.symbolTable.referenceFunction(descriptor.findActualForExpect()).owner private fun IrClass.findActualForExpected(): IrClass = context.ir.symbols.symbolTable.referenceClass(descriptor.findActualForExpect()).owner diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DFGBuilder.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DFGBuilder.kt index 91b7d5d0d86..7df14305b04 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DFGBuilder.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/DFGBuilder.kt @@ -587,7 +587,7 @@ internal class ModuleDFGBuilder(val context: Context, val irModule: IrModuleFrag expressionToEdge(value.getValueArgument(0)!!), expressionToEdge(value.getValueArgument(1)!!)) else -> { - val callee = value.symbol.owner as IrFunction + val callee = value.symbol.owner val arguments = value.getArguments() .map { expressionToEdge(it.second) } .let { diff --git a/runtime/src/launcher/cpp/androidLauncher.cpp b/runtime/src/launcher/cpp/androidLauncher.cpp index 4c22926c615..ba308d358b2 100644 --- a/runtime/src/launcher/cpp/androidLauncher.cpp +++ b/runtime/src/launcher/cpp/androidLauncher.cpp @@ -214,7 +214,6 @@ extern "C" void RUNTIME_USED Konan_main( if (launchThread) { launcherState = (LauncherState*)calloc(sizeof(LauncherState), 1); launcherState->nativeActivityState = {activity, savedState, savedStateSize, nullptr}; - activity->instance = launcherState; activity->callbacks->onDestroy = onDestroy; activity->callbacks->onStart = onStart; diff --git a/samples/androidNativeActivity/README.md b/samples/androidNativeActivity/README.md index 5f6aabcd47c..42f3a0ca919 100644 --- a/samples/androidNativeActivity/README.md +++ b/samples/androidNativeActivity/README.md @@ -1,12 +1,14 @@ # Android Native Activity - This example shows how to build an Android Native Activity. + This example shows how to build an Android Native Activity. Also, we provide an example +bridging mechanism for the Java APIs, callable from Native side. The example will render a textured dodecahedron using OpenGL ES library. It can be rotated with fingers. Please make sure that Android SDK version 25 is installed, using Android SDK manager in Android Studio. See https://developer.android.com/studio/index.html for more details on Android Studio or `$ANDROID_HOME/tools/bin/sdkmanager "platforms;android-25" "build-tools;25.0.2"` from command line. +We use JniBridge to call vibration service on the Java side for short tremble on startup. -To build use `ANDROID_HOME= ../gradlew buildApk`. +To build use `ANDROID_HOME= ../gradlew build`. Run `$ANDROID_HOME/platform-tools/adb install -r build/outputs/apk/debug/androidNativeActivity-debug.apk` to deploy the apk on the Android device or emulator (note that only ARM-based devices are currently supported). diff --git a/samples/androidNativeActivity/android.def b/samples/androidNativeActivity/android.def deleted file mode 100644 index 86750ea3392..00000000000 --- a/samples/androidNativeActivity/android.def +++ /dev/null @@ -1,8 +0,0 @@ -// TODO: androidLauncher.h will become part of the platform library. -headers = stdio.h stdlib.h string.h math.h time.h android/log.h androidLauncher.h EGL/egl.h GLES/gl.h -linkerOpst = -landroid -lEGL -lGLESv1_CM -lz ---- - -static int interop_errno() { - return errno; -} diff --git a/samples/androidNativeActivity/androidLauncher.h b/samples/androidNativeActivity/androidLauncher.h deleted file mode 100644 index d1a3cb359ff..00000000000 --- a/samples/androidNativeActivity/androidLauncher.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2010-2017 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef ANDROID_LAUNCHER_H -#define ANDROID_LAUNCHER_H - -#include - -#include -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -struct NativeActivityState { - struct ANativeActivity* activity; - void* savedState; - size_t savedStateSize; - struct ALooper* looper; -}; - -void getNativeActivityState(struct NativeActivityState* state); - -void notifySysEventProcessed(); - -#define LOOPER_ID_SYS 1 - -typedef enum NativeActivityEventKind { - UNKNOWN, - DESTROY, - START, - RESUME, - SAVE_INSTANCE_STATE, - PAUSE, - STOP, - CONFIGURATION_CHANGED, - LOW_MEMORY, - WINDOW_GAINED_FOCUS, - WINDOW_LOST_FOCUS, - NATIVE_WINDOW_CREATED, - NATIVE_WINDOW_DESTROYED, - INPUT_QUEUE_CREATED, - INPUT_QUEUE_DESTROYED -} NativeActivityEventKind; - -struct NativeActivityEvent { - NativeActivityEventKind eventKind; -}; - -struct NativeActivitySaveStateEvent { - NativeActivityEventKind eventKind; - void* savedState; - size_t savedStateSize; -}; - -struct NativeActivityWindowEvent { - NativeActivityEventKind eventKind; - struct ANativeWindow* window; -}; - -struct NativeActivityQueueEvent { - NativeActivityEventKind eventKind; - struct AInputQueue* queue; -}; - -#ifdef __cplusplus -} -#endif - -#endif // ANDROID_LAUNCHER_H \ No newline at end of file diff --git a/samples/androidNativeActivity/build.gradle b/samples/androidNativeActivity/build.gradle index 8f71734774f..af7bde516eb 100644 --- a/samples/androidNativeActivity/build.gradle +++ b/samples/androidNativeActivity/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:${project.property('konan.plugin.version')}" - classpath "com.android.tools.build:gradle:3.1.0" + classpath 'com.android.tools.build:gradle:3.1.2' } } diff --git a/samples/androidNativeActivity/src/main/AndroidManifest.xml b/samples/androidNativeActivity/src/main/AndroidManifest.xml index e9c2ffe931c..d369e33a24f 100644 --- a/samples/androidNativeActivity/src/main/AndroidManifest.xml +++ b/samples/androidNativeActivity/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ android:versionCode="1" android:versionName="1.0"> + + memberAt(offset: Long): T { + return interpretPointed(this.rawPtr + offset) + } + + val magic get() = memberAt(0).value.toInt() + val size get() = memberAt(2).value + val zero get() = memberAt(6).value + val width get() = memberAt(18).value + val height get() = memberAt(22).value + val bits get() = memberAt(28).value.toInt() + val data get() = interpretCPointer(rawPtr + 54) as CArrayPointer +} \ No newline at end of file diff --git a/samples/androidNativeActivity/src/main/kotlin/Disposable.kt b/samples/androidNativeActivity/src/main/kotlin/Disposable.kt new file mode 100644 index 00000000000..1e7f912d163 --- /dev/null +++ b/samples/androidNativeActivity/src/main/kotlin/Disposable.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package NativeApplication + +import kotlinx.cinterop.* + +/** + * Disposable class manages and owns all its native resources. It allocates some resources + * during construction and may allocate some additional resources during operation. + * It must free all its resource once [dispose] is invoked. "Disposed" is a final state of the disposable + * class. It is not supposed to be used after being disposed. + */ +interface Disposable { + /** + * Disposes all native resources owned by this class. This function must be invoked + * exactly once as the last operation on the corresponding class. + */ + fun dispose() +} + +/** + * Helper class to implement [Disposable] interface. It contains an [arena] for native + * memory allocations and a number of helper methods to simplify management of other + * kinds of native resources. + * + * It is important to wrap all potentially exception-throwing code in the class constructor + * into [tryConstruct] invocation, because, when object construction fails with exception, + * it ensures that all the resource that were allocated so far will get freed. + */ +abstract class DisposableContainer : Disposable { + val arena = Arena() + + override fun dispose() { + arena.clear() + } + + inline fun tryConstruct(init: () -> T): T = + try { init() } + catch (e: Throwable) { + dispose() + throw e + } + + inline fun disposable( + message: String = "disposable", + create: () -> T?, + crossinline dispose: (T) -> Unit + ): T = + tryConstruct { + create()?.also { + arena.defer { dispose(it) } + } ?: throw Error(message) + } + + inline fun disposable(create: () -> T): T = + disposable( + create = create, + dispose = { it.dispose() }) +} diff --git a/samples/androidNativeActivity/src/main/kotlin/engine.kt b/samples/androidNativeActivity/src/main/kotlin/Engine.kt similarity index 65% rename from samples/androidNativeActivity/src/main/kotlin/engine.kt rename to samples/androidNativeActivity/src/main/kotlin/Engine.kt index 599629e3cc1..8b8dbbba0ac 100644 --- a/samples/androidNativeActivity/src/main/kotlin/engine.kt +++ b/samples/androidNativeActivity/src/main/kotlin/Engine.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2017 JetBrains s.r.o. + * Copyright 2010-2018 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package NativeApplication import kotlinx.cinterop.* import platform.android.* @@ -28,27 +29,14 @@ fun logInfo(message: String) { __android_log_write(ANDROID_LOG_INFO, "KonanActivity", message) } -val errno: Int - get() = posix_errno() - -fun getUnixError() = strerror(errno)!!.toKString() +private fun getUnixError() = strerror(posix_errno())!!.toKString() const val LOOPER_ID_INPUT = 2 -fun main(args: Array) { - logInfo("Hello world!") - memScoped { - val state = alloc() - getNativeActivityState(state.ptr) - val engine = Engine(this, state) - engine.mainLoop() - } -} - inline fun CPointer<*>?.dereferenceAs(): T = this!!.reinterpret().pointed -class Engine(val arena: NativePlacement, val state: NativeActivityState) { - private val renderer = Renderer(arena, state.activity!!.pointed, state.savedState) +class Engine(val state: NativeActivityState): DisposableContainer() { + private val renderer = Renderer(this, state.activity!!.pointed, state.savedState) private var queue: CPointer? = null private var rendererState: COpaquePointer? = null @@ -63,22 +51,23 @@ class Engine(val arena: NativePlacement, val state: NativeActivityState) { private var animating = false private val pointerSize = CPointerVar.size + private val now = arena.alloc() + private val eventPointer = arena.alloc() + private val inputEvent = arena.alloc>() + fun mainLoop() { + callToManagedAPI() + + val fd = arena.alloc() while (true) { // Process events. - memScoped { - val fd = alloc() - eventLoop@while (true) { - val id = ALooper_pollAll(if (needRedraw || animating) 0 else -1, fd.ptr, null, null) - if (id < 0) break@eventLoop - when (id) { - LOOPER_ID_SYS -> { - if (!processSysEvent(fd)) - return // An error occured. - } - - LOOPER_ID_INPUT -> processUserInput() - } + eventLoop@ while (true) { + val id = ALooper_pollAll(if (needRedraw || animating) 0 else -1, fd.ptr, null, null) + if (id < 0) break@eventLoop + when (id) { + LOOPER_ID_SYS -> if (!processSysEvent(fd)) return // An error occured. + LOOPER_ID_INPUT -> processUserInput() + else -> logError("Unprocessed event: $id") } } when { @@ -97,8 +86,29 @@ class Engine(val arena: NativePlacement, val state: NativeActivityState) { } } - private fun processSysEvent(fd: IntVar): Boolean = memScoped { - val eventPointer = alloc() + override fun dispose() { + renderer.destroy() + super.dispose() + } + + private val jniBrigde = JniBridge(state.activity!!.pointed.vm!!) + + private fun callToManagedAPI() = jniBrigde.withLocalFrame { + // Actually, this is a context pointer. + val context = JniObject(state.activity!!.pointed.clazz!!) + + val contextClass = FindClass("android/content/Context") + val getSystemServiceMethod = GetMethodID( + contextClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;")!! + val vibrator = CallObjectMethod(context, getSystemServiceMethod, "vibrator") + if (vibrator != null) { + val vibratorClass = FindClass("android/os/Vibrator") + val vibrateMethod = GetMethodID(vibratorClass, "vibrate", "(J)V")!! + CallVoidMethod(vibrator, vibrateMethod, 500L) + } + } + + private fun processSysEvent(fd: IntVar): Boolean { val readBytes = read(fd.value, eventPointer.ptr, pointerSize.narrow()).toLong() if (readBytes != pointerSize.toLong()) { logError("Failure reading event, $readBytes read: ${getUnixError()}") @@ -106,11 +116,22 @@ class Engine(val arena: NativePlacement, val state: NativeActivityState) { } try { val event = eventPointer.value.dereferenceAs() + println("got ${event.eventKind}") when (event.eventKind) { - NativeActivityEventKind.START -> logInfo("START event received") + NativeActivityEventKind.START -> { + logInfo("Activity started") + renderer.start() + } + + NativeActivityEventKind.STOP -> { + renderer.stop() + } NativeActivityEventKind.DESTROY -> { - rendererState?.let { free(it) } + rendererState?.let { + free(it) + rendererState = null + } return false } @@ -145,6 +166,7 @@ class Engine(val arena: NativePlacement, val state: NativeActivityState) { val dataSize = state.second.signExtend() rendererState = realloc(rendererState, dataSize) memcpy(rendererState, state.first, dataSize) + logInfo("Saving instance state to $rendererState: $dataSize bytes") saveStateEvent.savedState = rendererState saveStateEvent.savedStateSize = dataSize } @@ -156,11 +178,8 @@ class Engine(val arena: NativePlacement, val state: NativeActivityState) { } private fun getTime(): Float { - memScoped { - val now = alloc() - clock_gettime(CLOCK_MONOTONIC, now.ptr) - return now.tv_sec + now.tv_nsec / 1_000_000_000.0f - } + clock_gettime(CLOCK_MONOTONIC, now.ptr) + return now.tv_sec + now.tv_nsec / 1_000_000_000.0f } private fun getEventPoint(event: CPointer?, i: Int) = @@ -169,27 +188,26 @@ class Engine(val arena: NativePlacement, val state: NativeActivityState) { private fun getEventTime(event: CPointer?) = AMotionEvent_getEventTime(event) / 1_000_000_000.0f - private fun processUserInput(): Unit = memScoped { - logInfo("Processing user input") - val event = alloc>() - if (AInputQueue_getEvent(queue, event.ptr) < 0) { + private fun processUserInput(): Unit { + if (AInputQueue_getEvent(queue, inputEvent.ptr) < 0) { logError("Failure reading input event") return } - val eventType = AInputEvent_getType(event.value) + val event = inputEvent.value + val eventType = AInputEvent_getType(event) if (eventType == AINPUT_EVENT_TYPE_MOTION) { - val action = AKeyEvent_getAction(event.value) and AMOTION_EVENT_ACTION_MASK + val action = AKeyEvent_getAction(event) and AMOTION_EVENT_ACTION_MASK when (action) { AMOTION_EVENT_ACTION_DOWN -> { animating = false - currentPoint = getEventPoint(event.value, 0) - startTime = getEventTime(event.value) + currentPoint = getEventPoint(event, 0) + startTime = getEventTime(event) startPoint = currentPoint } AMOTION_EVENT_ACTION_UP -> { - val endPoint = getEventPoint(event.value, 0) - val endTime = getEventTime(event.value) + val endPoint = getEventPoint(event, 0) + val endTime = getEventTime(event) animating = true velocity = (endPoint - startPoint) / (endTime - startTime + 1e-9f) if (velocity.length > renderer.screen.length) @@ -202,13 +220,13 @@ class Engine(val arena: NativePlacement, val state: NativeActivityState) { } AMOTION_EVENT_ACTION_MOVE -> { - val numberOfPointers = AMotionEvent_getPointerCount(event.value).toInt() + val numberOfPointers = AMotionEvent_getPointerCount(event).toInt() for (i in 0 until numberOfPointers) - move(getEventPoint(event.value, i)) + move(getEventPoint(event, i)) } } } - AInputQueue_finishEvent(queue, event.value, 1) + AInputQueue_finishEvent(queue, event, 1) } private fun move(newPoint: Vector2) { diff --git a/samples/androidNativeActivity/src/main/kotlin/JniBridge.kt b/samples/androidNativeActivity/src/main/kotlin/JniBridge.kt new file mode 100644 index 00000000000..635e1193b0b --- /dev/null +++ b/samples/androidNativeActivity/src/main/kotlin/JniBridge.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package NativeApplication + +import kotlinx.cinterop.* +import platform.android.* + +data class JniClass(val jclass: jclass) +data class JniObject(val jobject: jobject) +data class JniMethod(val jmethod: jmethodID) + +fun asJniClass(jclass: jclass?) = + if (jclass != null) JniClass(jclass) else null +fun asJniObject(jobject: jobject?) = + if (jobject != null) JniObject(jobject) else null +fun asJniMethod(jmethodID: jmethodID?) = + if (jmethodID != null) JniMethod(jmethodID) else null + +class JniBridge(val vm: CPointer) { + private val vmFunctions: JNIInvokeInterface = vm.pointed.pointed!! + val jniEnv = memScoped { + val envStorage = alloc>() + if (vmFunctions.AttachCurrentThreadAsDaemon!!(vm, envStorage.ptr, null) != 0) + throw Error("Cannot attach thread to the VM") + envStorage.value!! + } + private val envFunctions: JNINativeInterface = jniEnv.pointed.pointed!! + + // JNI operations. + private val fNewStringUTF = envFunctions.NewStringUTF!! + private val fFindClass = envFunctions.FindClass!! + private val fGetMethodID = envFunctions.GetMethodID!! + private val fCallVoidMethodA = envFunctions.CallVoidMethodA!! + private val fCallObjectMethodA = envFunctions.CallObjectMethodA!! + private val fExceptionCheck = envFunctions.ExceptionCheck!! + private val fExceptionDescribe = envFunctions.ExceptionDescribe!! + private val fExceptionClear = envFunctions.ExceptionClear!! + val fPushLocalFrame = envFunctions.PushLocalFrame!! + val fPopLocalFrame = envFunctions.PopLocalFrame!! + + private fun check() { + if (fExceptionCheck(jniEnv) != 0.toByte()) { + fExceptionDescribe(jniEnv) + fExceptionClear(jniEnv) + throw Error("JVM exception thrown") + } + } + + fun toJString(string: String) = memScoped { + val result = asJniObject(fNewStringUTF(jniEnv, string.cstr.ptr)) + check() + result + } + + fun toJValues(arguments: Array, scope: MemScope): CPointer? { + val result = scope.allocArray(arguments.size) + arguments.mapIndexed { index, it -> + when (it) { + null -> result[index].l = null + is JniObject -> result[index].l = it.jobject + is String -> result[index].l = toJString(it)?.jobject + is Int -> result[index].i = it + is Long -> result[index].j = it + else -> throw Error("Unsupported conversion for ${it::class.simpleName}") + } + } + return result + } + + fun FindClass(name: String) = memScoped { + val result = asJniClass(fFindClass(jniEnv, name.cstr.ptr)) + check() + result + } + + fun GetMethodID(clazz: JniClass?, name: String, signature: String) = memScoped { + val result = asJniMethod(fGetMethodID(jniEnv, clazz?.jclass, name.cstr.ptr, signature.cstr.ptr)) + check() + result + } + + fun CallVoidMethod(receiver: JniObject?, method: JniMethod, vararg arguments: Any?) = memScoped { + fCallVoidMethodA(jniEnv, receiver?.jobject, method.jmethod, + toJValues(arguments, this@memScoped)) + check() + } + + fun CallObjectMethod(receiver: JniObject?, method: JniMethod, vararg arguments: Any?) = memScoped { + val result = asJniObject(fCallObjectMethodA(jniEnv, receiver?.jobject, method.jmethod, + toJValues(arguments, this@memScoped))) + check() + result + } + + // Usually, use this + inline fun withLocalFrame(block: JniBridge.() -> T): T { + if (fPushLocalFrame(jniEnv, 0) < 0) + throw Error("Cannot push new local frame") + try { + return block() + } finally { + fPopLocalFrame(jniEnv, null) + } + } +} diff --git a/samples/androidNativeActivity/src/main/kotlin/polyhedra.kt b/samples/androidNativeActivity/src/main/kotlin/Polyhedra.kt similarity index 98% rename from samples/androidNativeActivity/src/main/kotlin/polyhedra.kt rename to samples/androidNativeActivity/src/main/kotlin/Polyhedra.kt index 3eaa9962b39..dfb4beba9f8 100644 --- a/samples/androidNativeActivity/src/main/kotlin/polyhedra.kt +++ b/samples/androidNativeActivity/src/main/kotlin/Polyhedra.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2017 JetBrains s.r.o. + * Copyright 2010-2018 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package NativeApplication const val Zero = 0.0f const val DodeA = 0.93417235896f // (Sqrt(5) + 1) / (2 * Sqrt(3)) diff --git a/samples/androidNativeActivity/src/main/kotlin/renderer.kt b/samples/androidNativeActivity/src/main/kotlin/Renderer.kt similarity index 62% rename from samples/androidNativeActivity/src/main/kotlin/renderer.kt rename to samples/androidNativeActivity/src/main/kotlin/Renderer.kt index 201879151ab..c8ca1354a8b 100644 --- a/samples/androidNativeActivity/src/main/kotlin/renderer.kt +++ b/samples/androidNativeActivity/src/main/kotlin/Renderer.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2017 JetBrains s.r.o. + * Copyright 2010-2018 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ * limitations under the License. */ +package NativeApplication + import kotlinx.cinterop.* import platform.android.* import platform.egl.* @@ -55,10 +57,9 @@ import platform.gles.glTexCoordPointer import platform.gles.glNormalPointer import platform.gles.GL_TEXTURE_ENV_COLOR - -class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActivity, val savedMatrix: COpaquePointer?) { - - private val arena = MemScope() +class Renderer(val container: DisposableContainer, + val nativeActivity: ANativeActivity, + val savedMatrix: COpaquePointer?) { private var display: EGLDisplay? = null private var surface: EGLSurface? = null private var context: EGLContext? = null @@ -66,20 +67,20 @@ class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActi var screen = Vector2.Zero - private val matrix = parentArena.allocArray(16) + private val matrix = container.arena.allocArray(16) init { if (savedMatrix != null) { memcpy(matrix, savedMatrix, 16 * 4) } else { - for (i in 0..3) - for (j in 0..3) + for (i in 0 .. 3) + for (j in 0 .. 3) matrix[i * 4 + j] = if (i == j) 1.0f else 0.0f } } fun initialize(window: CPointer): Boolean { - with(arena) { + with (container.arena) { logInfo("Initializing context..") display = eglGetDisplay(null) if (display == null) { @@ -100,15 +101,11 @@ class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActi ) val numConfigs = alloc() if (eglChooseConfig(display, attribs, null, 0, numConfigs.ptr) == 0) { - logError("eglChooseConfig()#1 returned error ${eglGetError()}") - destroy() - return false + throw Error("eglChooseConfig()#1 returned error ${eglGetError()}") } val supportedConfigs = allocArray(numConfigs.value) if (eglChooseConfig(display, attribs, supportedConfigs, numConfigs.value, numConfigs.ptr) == 0) { - logError("eglChooseConfig()#2 returned error ${eglGetError()}") - destroy() - return false + throw Error("eglChooseConfig()#2 returned error ${eglGetError()}") } var configIndex = 0 while (configIndex < numConfigs.value) { @@ -116,13 +113,11 @@ class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActi val g = alloc() val b = alloc() val d = alloc() - if (eglGetConfigAttrib(display, supportedConfigs[configIndex], EGL_RED_SIZE, r.ptr) != 0 && - eglGetConfigAttrib(display, supportedConfigs[configIndex], EGL_GREEN_SIZE, g.ptr) != 0 && - eglGetConfigAttrib(display, supportedConfigs[configIndex], EGL_BLUE_SIZE, b.ptr) != 0 && - eglGetConfigAttrib(display, supportedConfigs[configIndex], EGL_DEPTH_SIZE, d.ptr) != 0 && - r.value == 8 && g.value == 8 && b.value == 8 && d.value == 0 ) { - break - } + if (eglGetConfigAttrib(display, supportedConfigs[configIndex], EGL_RED_SIZE, r.ptr) != 0 && + eglGetConfigAttrib(display, supportedConfigs[configIndex], EGL_GREEN_SIZE, g.ptr) != 0 && + eglGetConfigAttrib(display, supportedConfigs[configIndex], EGL_BLUE_SIZE, b.ptr) != 0 && + eglGetConfigAttrib(display, supportedConfigs[configIndex], EGL_DEPTH_SIZE, d.ptr) != 0 && + r.value == 8 && g.value == 8 && b.value == 8 && d.value == 0) break ++configIndex } if (configIndex >= numConfigs.value) @@ -130,31 +125,23 @@ class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActi surface = eglCreateWindowSurface(display, supportedConfigs[configIndex], window, null) if (surface == null) { - logError("eglCreateWindowSurface() returned error ${eglGetError()}") - destroy() - return false + throw Error("eglCreateWindowSurface() returned error ${eglGetError()}") } context = eglCreateContext(display, supportedConfigs[configIndex], null, null) if (context == null) { - logError("eglCreateContext() returned error ${eglGetError()}") - destroy() - return false + throw Error("eglCreateContext() returned error ${eglGetError()}") } if (eglMakeCurrent(display, surface, surface, context) == 0) { - logError("eglMakeCurrent() returned error ${eglGetError()}") - destroy() - return false + throw Error("eglMakeCurrent() returned error ${eglGetError()}") } val width = alloc() val height = alloc() if (eglQuerySurface(display, surface, EGL_WIDTH, width.ptr) == 0 || eglQuerySurface(display, surface, EGL_HEIGHT, height.ptr) == 0) { - logError("eglQuerySurface() returned error ${eglGetError()}") - destroy() - return false + throw Error("eglQuerySurface() returned error ${eglGetError()}") } this@Renderer.screen = Vector2(width.value.toFloat(), height.value.toFloat()) @@ -192,14 +179,14 @@ class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActi fun getState() = matrix to 16 * 4 - fun rotateBy(vec: Vector2) { + fun rotateBy(vector: Vector2) { if (!initialized) return - val len = vec.length - if (len < 1e-9f) return - val angle = 180 * len / screen.length - val x = - vec.y / len - val y = - vec.x / len + val length = vector.length + if (length < 1e-9f) return + val angle = 180 * length / screen.length + val x = -vector.y / length + val y = -vector.x / length glPushMatrix() glMatrixMode(GL_MODELVIEW) @@ -210,51 +197,38 @@ class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActi glPopMatrix() } - private class BMPHeader(val rawPtr: NativePtr) { - inline fun memberAt(offset: Long): T { - return interpretPointed(this.rawPtr + offset) - } - val magic get() = memberAt(0).value.toInt() - val size get() = memberAt(2).value - val zero get() = memberAt(6).value - val width get() = memberAt(18).value - val height get() = memberAt(22).value - val bits get() = memberAt(28).value.toInt() - val data get() = interpretCPointer(rawPtr + 54) as CArrayPointer - } private fun loadTexture(assetName: String): Unit = memScoped { val asset = AAssetManager_open(nativeActivity.assetManager, assetName, AASSET_MODE_BUFFER) - if (asset == null) { - logError("Error opening asset") - return - } - val length = AAsset_getLength(asset) - val buf = allocArray(length) - if (AAsset_read(asset, buf, length) != length.toInt()) { - logError("Error reading asset") - AAsset_close(asset) - } - with (BMPHeader(buf.rawValue)) { - if (magic != 0x4d42 || zero != 0 || size != length.toInt() || bits != 24) { - logError("Error parsing texture file") - AAsset_close(asset) - return + ?: throw Error("Error opening asset $assetName") + try { + val length = AAsset_getLength(asset) + val buffer = allocArray(length) + if (AAsset_read(asset, buffer, length) != length.toInt()) { + throw Error("Error reading asset") } - val numberOfBytes = width * height * 3 - for (i in 0 until numberOfBytes step 3) { - val t = data[i] - data[i] = data[i + 2] - data[i + 2] = t + with(BMPHeader(buffer.rawValue)) { + if (magic != 0x4d42 || zero != 0 || size != length.toInt() || bits != 24) { + throw Error("Error parsing texture file") + } + val numberOfBytes = width * height * 3 + // Swap BGR in bitmap to RGB. + for (i in 0 until numberOfBytes step 3) { + val t = data[i] + data[i] = data[i + 2] + data[i + 2] = t + } + glBindTexture(GL_TEXTURE_2D, 1) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND.toFloat()) + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, cValuesOf(1.0f, 1.0f, 1.0f, 1.0f)) + glPixelStorei(GL_UNPACK_ALIGNMENT, 1) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data) + } - glBindTexture(GL_TEXTURE_2D, 1) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND.toFloat()) - glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, cValuesOf(1.0f, 1.0f, 1.0f, 1.0f)) - glPixelStorei(GL_UNPACK_ALIGNMENT, 1) - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data) + } finally { AAsset_close(asset) } } @@ -265,7 +239,7 @@ class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActi private val scale = 1.25f - fun draw() = memScoped { + fun draw(): Unit { if (!initialized) return glPushMatrix() @@ -279,61 +253,83 @@ class Renderer(val parentArena: NativePlacement, val nativeActivity: ANativeActi glEnableClientState(GL_NORMAL_ARRAY) glEnableClientState(GL_TEXTURE_COORD_ARRAY) - val poly = RegularPolyhedra.Dodecahedron + val polygon = RegularPolyhedra.Dodecahedron val vertices = mutableListOf() val texCoords = mutableListOf() val triangles = mutableListOf() val normals = mutableListOf() - for (face in poly.faces) { - val u = poly.vertices[face[2].toInt()] - poly.vertices[face[1].toInt()] - val v = poly.vertices[face[0].toInt()] - poly.vertices[face[1].toInt()] + for (face in polygon.faces) { + val u = polygon.vertices[face[2].toInt()] - polygon.vertices[face[1].toInt()] + val v = polygon.vertices[face[0].toInt()] - polygon.vertices[face[1].toInt()] val normal = u.crossProduct(v).normalized() val copiedFace = ByteArray(face.size) for (j in face.indices) { copiedFace[j] = (vertices.size / 4).toByte() - poly.vertices[face[j].toInt()].copyCoordinatesTo(vertices) + polygon.vertices[face[j].toInt()].copyCoordinatesTo(vertices) vertices.add(scale) normal.copyCoordinatesTo(normals) texturePoints[j].copyCoordinatesTo(texCoords) } - for (j in 1..face.size-2) { + for (j in 1..face.size - 2) { triangles.add(copiedFace[0]) triangles.add(copiedFace[j]) triangles.add(copiedFace[j + 1]) } } - glFrontFace(GL_CW) - glVertexPointer(4, GL_FLOAT, 0, vertices.toFloatArray().toCValues().ptr) - glTexCoordPointer(2, GL_FLOAT, 0, texCoords.toFloatArray().toCValues().ptr) - glNormalPointer(GL_FLOAT, 0, normals.toFloatArray().toCValues().ptr) - glDrawElements(GL_TRIANGLES, triangles.size, GL_UNSIGNED_BYTE, triangles.toByteArray().toCValues().ptr) + memScoped { + glFrontFace(GL_CW) + glVertexPointer(4, GL_FLOAT, 0, vertices.toFloatArray().toCValues().ptr) + glTexCoordPointer(2, GL_FLOAT, 0, texCoords.toFloatArray().toCValues().ptr) + glNormalPointer(GL_FLOAT, 0, normals.toFloatArray().toCValues().ptr) + glDrawElements(GL_TRIANGLES, triangles.size, GL_UNSIGNED_BYTE, triangles.toByteArray().toCValues().ptr) + } glPopMatrix() if (eglSwapBuffers(display, surface) == 0) { - logError("eglSwapBuffers() returned error ${eglGetError()}") - destroy() + val error = eglGetError() + if (error != EGL_BAD_SURFACE) + throw Error("eglSwapBuffers() returned error $error") + else { + if (eglMakeCurrent(display, surface, surface, context) == 0) { + throw Error("Reinit eglMakeCurrent() returned error ${eglGetError()}") + } + if (eglSwapBuffers(display, surface) == 0) + throw Error("Bad eglSwapBuffers() after surface reinit: ${eglGetError()}") + } } } + fun start() { + logInfo("Starting renderer..") + if (initialized) { + if (eglMakeCurrent(display, surface, surface, context) == 0) { + throw Error("eglMakeCurrent() returned error ${eglGetError()}") + } + } + } + + fun stop() { + logInfo("Stopping renderer..") + eglMakeCurrent(display, null, null, null) + } + fun destroy() { if (!initialized) return - logInfo("Destroying context..") - + logInfo("Destroying renderer..") eglMakeCurrent(display, null, null, null) + context?.let { eglDestroyContext(display, it) } surface?.let { eglDestroySurface(display, it) } - eglTerminate(display) + display?.let { eglTerminate(display) } display = null surface = null context = null initialized = false - // TODO: What should be called here? - //arena.clear() } } diff --git a/samples/androidNativeActivity/src/main/kotlin/vectors.kt b/samples/androidNativeActivity/src/main/kotlin/Vectors.kt similarity index 96% rename from samples/androidNativeActivity/src/main/kotlin/vectors.kt rename to samples/androidNativeActivity/src/main/kotlin/Vectors.kt index 062661c51b9..91f5efc79dd 100644 --- a/samples/androidNativeActivity/src/main/kotlin/vectors.kt +++ b/samples/androidNativeActivity/src/main/kotlin/Vectors.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2017 JetBrains s.r.o. + * Copyright 2010-2018 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package NativeApplication import kotlinx.cinterop.* import platform.android.* diff --git a/samples/androidNativeActivity/src/main/kotlin/main.kt b/samples/androidNativeActivity/src/main/kotlin/main.kt new file mode 100644 index 00000000000..71a640a46d4 --- /dev/null +++ b/samples/androidNativeActivity/src/main/kotlin/main.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import kotlinx.cinterop.* +import platform.android.* +import NativeApplication.* + +fun main(args: Array) { + logInfo("Entering main().") + memScoped { + val state = alloc() + getNativeActivityState(state.ptr) + val engine = Engine(state) + try { + engine.mainLoop() + } finally { + engine.dispose() + } + } + kotlin.system.exitProcess(0) +}