Improve Android native sample, add basic Java bridge. (#1606)

This commit is contained in:
Nikolay Igotti
2018-05-24 18:20:59 +03:00
committed by GitHub
parent 41ea12a4e7
commit 5b087ae04c
20 changed files with 445 additions and 261 deletions
@@ -57,7 +57,7 @@ internal class KonanIr(context: Context, irModule: IrModuleFragment): Ir<Context
fun get(descriptor: FunctionDescriptor): IrFunction {
return moduleIndexForCodegen.functions[descriptor]
?: symbols.symbolTable.referenceFunction(descriptor).owner as IrFunction
?: symbols.symbolTable.referenceFunction(descriptor).owner
}
fun get(descriptor: ClassDescriptor): IrClass {
@@ -1690,7 +1690,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
//-------------------------------------------------------------------------//
private fun evaluateSpecialIntrinsicCall(expression: IrFunctionAccessExpression): LLVMValueRef? {
val function = expression.symbol.owner as IrFunction
val function = expression.symbol.owner
if (function.isIntrinsic) {
when (function.descriptor) {
@@ -1715,7 +1715,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
val initializer = callee.getValueArgument(1) as IrCall
val thiz = evaluateExpression(callee.getValueArgument(0)!!)
evaluateSimpleFunctionCall(
initializer.symbol.owner as IrFunction,
initializer.symbol.owner,
listOf(thiz) + evaluateExplicitArgs(initializer),
resultLifetime(initializer)
)
@@ -1901,7 +1901,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
assert (expression.getArguments().isEmpty())
val descriptor = expression.symbol.owner as IrFunction
val descriptor = expression.symbol.owner
assert (descriptor.dispatchReceiverParameter == null)
val entry = codegen.functionEntryPointAddress(descriptor)
@@ -1983,7 +1983,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
private fun evaluateFunctionCall(callee: IrCall, args: List<LLVMValueRef>,
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<IrE
)
}
return call(callee.symbol.owner as IrFunction, function, args, resultLifetime)
return call(callee.symbol.owner, function, args, resultLifetime)
}
//-------------------------------------------------------------------------//
@@ -2086,7 +2086,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
} else {
functionGenerationContext.allocInstance(constructedClass, resultLifetime(callee))
}
evaluateSimpleFunctionCall(callee.symbol.owner as IrFunction,
evaluateSimpleFunctionCall(callee.symbol.owner,
listOf(thisValue) + args, Lifetime.IRRELEVANT /* constructor doesn't return anything */)
thisValue
}
@@ -2401,7 +2401,7 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map<IrE
private fun evaluateOperatorCall(callee: IrCall, args: List<LLVMValueRef>): 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) {
@@ -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())
@@ -295,7 +295,7 @@ class DefaultParameterInjector constructor(val context: CommonBackendContext): B
private fun parametersForCall(expression: IrFunctionAccessExpression): Pair<IrFunctionSymbol, List<Pair<ValueParameterDescriptor, IrExpression?>>> {
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
@@ -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
@@ -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 {
@@ -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;
+4 -2
View File
@@ -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=<your path to android sdk> ../gradlew buildApk`.
To build use `ANDROID_HOME=<your path to android sdk> ../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).
@@ -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;
}
@@ -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 <unistd.h>
#include <android/configuration.h>
#include <android/looper.h>
#include <android/native_activity.h>
#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
+1 -1
View File
@@ -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'
}
}
@@ -5,6 +5,8 @@
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.VIBRATE" />
<!-- This .apk has no Java code itself, so set hasCode to false. -->
<application
android:allowBackup="false"
@@ -0,0 +1,33 @@
/*
* 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.*
internal class BMPHeader(val rawPtr: NativePtr) {
inline fun <reified T : CPointed> memberAt(offset: Long): T {
return interpretPointed<T>(this.rawPtr + offset)
}
val magic get() = memberAt<ShortVar>(0).value.toInt()
val size get() = memberAt<IntVar>(2).value
val zero get() = memberAt<IntVar>(6).value
val width get() = memberAt<IntVar>(18).value
val height get() = memberAt<IntVar>(22).value
val bits get() = memberAt<ShortVar>(28).value.toInt()
val data get() = interpretCPointer<ByteVar>(rawPtr + 54) as CArrayPointer<ByteVar>
}
@@ -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 <T> tryConstruct(init: () -> T): T =
try { init() }
catch (e: Throwable) {
dispose()
throw e
}
inline fun <T> disposable(
message: String = "disposable",
create: () -> T?,
crossinline dispose: (T) -> Unit
): T =
tryConstruct {
create()?.also {
arena.defer { dispose(it) }
} ?: throw Error(message)
}
inline fun <T : Disposable> disposable(create: () -> T): T =
disposable(
create = create,
dispose = { it.dispose() })
}
@@ -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<String>) {
logInfo("Hello world!")
memScoped {
val state = alloc<NativeActivityState>()
getNativeActivityState(state.ptr)
val engine = Engine(this, state)
engine.mainLoop()
}
}
inline fun <reified T : CPointed> CPointer<*>?.dereferenceAs(): T = this!!.reinterpret<T>().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<AInputQueue>? = 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<timespec>()
private val eventPointer = arena.alloc<COpaquePointerVar>()
private val inputEvent = arena.alloc<CPointerVar<AInputEvent>>()
fun mainLoop() {
callToManagedAPI()
val fd = arena.alloc<IntVar>()
while (true) {
// Process events.
memScoped {
val fd = alloc<IntVar>()
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<COpaquePointerVar>()
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<NativeActivityEvent>()
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<size_t>()
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<timespec>()
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<AInputEvent>?, i: Int) =
@@ -169,27 +188,26 @@ class Engine(val arena: NativePlacement, val state: NativeActivityState) {
private fun getEventTime(event: CPointer<AInputEvent>?) =
AMotionEvent_getEventTime(event) / 1_000_000_000.0f
private fun processUserInput(): Unit = memScoped {
logInfo("Processing user input")
val event = alloc<CPointerVar<AInputEvent>>()
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) {
@@ -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<JavaVMVar>) {
private val vmFunctions: JNIInvokeInterface = vm.pointed.pointed!!
val jniEnv = memScoped {
val envStorage = alloc<CPointerVar<JNIEnvVar>>()
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<out Any?>, scope: MemScope): CPointer<jvalue>? {
val result = scope.allocArray<jvalue>(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 <T> withLocalFrame(block: JniBridge.() -> T): T {
if (fPushLocalFrame(jniEnv, 0) < 0)
throw Error("Cannot push new local frame")
try {
return block()
} finally {
fPopLocalFrame(jniEnv, null)
}
}
}
@@ -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))
@@ -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<FloatVar>(16)
private val matrix = container.arena.allocArray<FloatVar>(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<ANativeWindow>): 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<EGLintVar>()
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<EGLConfigVar>(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<EGLintVar>()
val b = alloc<EGLintVar>()
val d = alloc<EGLintVar>()
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<EGLintVar>()
val height = alloc<EGLintVar>()
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 <reified T : CPointed> memberAt(offset: Long): T {
return interpretPointed<T>(this.rawPtr + offset)
}
val magic get() = memberAt<ShortVar>(0).value.toInt()
val size get() = memberAt<IntVar>(2).value
val zero get() = memberAt<IntVar>(6).value
val width get() = memberAt<IntVar>(18).value
val height get() = memberAt<IntVar>(22).value
val bits get() = memberAt<ShortVar>(28).value.toInt()
val data get() = interpretCPointer<ByteVar>(rawPtr + 54) as CArrayPointer<ByteVar>
}
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<ByteVar>(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<ByteVar>(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<Float>()
val texCoords = mutableListOf<Float>()
val triangles = mutableListOf<Byte>()
val normals = mutableListOf<Float>()
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()
}
}
@@ -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.*
@@ -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<String>) {
logInfo("Entering main().")
memScoped {
val state = alloc<NativeActivityState>()
getNativeActivityState(state.ptr)
val engine = Engine(state)
try {
engine.mainLoop()
} finally {
engine.dispose()
}
}
kotlin.system.exitProcess(0)
}