mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-06-02 02:58:46 +02:00
668f623256
An exceptional signal handler allows us to convert an OS signal into a C++ exception, this allows us to alleviate a lot of crashes that would otherwise occur from signals being thrown during execution of games and be able to handle them gracefully.
443 lines
17 KiB
Kotlin
443 lines
17 KiB
Kotlin
/*
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
|
*/
|
|
|
|
package emu.skyline
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.graphics.PointF
|
|
import android.net.Uri
|
|
import android.os.*
|
|
import android.util.Log
|
|
import android.view.*
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.core.view.isGone
|
|
import androidx.core.view.isInvisible
|
|
import emu.skyline.input.*
|
|
import emu.skyline.loader.getRomFormat
|
|
import emu.skyline.utils.Settings
|
|
import kotlinx.android.synthetic.main.emu_activity.*
|
|
import java.io.File
|
|
import kotlin.math.abs
|
|
|
|
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTouchListener {
|
|
companion object {
|
|
private val Tag = EmulationActivity::class.java.simpleName
|
|
}
|
|
|
|
init {
|
|
System.loadLibrary("skyline") // libskyline.so
|
|
}
|
|
|
|
/**
|
|
* A map of [Vibrator]s that correspond to [InputManager.controllers]
|
|
*/
|
|
private var vibrators = HashMap<Int, Vibrator>()
|
|
|
|
/**
|
|
* The surface object used for displaying frames
|
|
*/
|
|
@Volatile
|
|
private var surface : Surface? = null
|
|
|
|
/**
|
|
* A boolean flag denoting if the emulation thread should call finish() or not
|
|
*/
|
|
@Volatile
|
|
private var shouldFinish : Boolean = true
|
|
|
|
/**
|
|
* The Kotlin thread on which emulation code executes
|
|
*/
|
|
private lateinit var emulationThread : Thread
|
|
|
|
private val settings by lazy { Settings(this) }
|
|
|
|
/**
|
|
* This is the entry point into the emulation code for libskyline
|
|
*
|
|
* @param romUri The URI of the ROM as a string, used to print out in the logs
|
|
* @param romType The type of the ROM as an enum value
|
|
* @param romFd The file descriptor of the ROM object
|
|
* @param preferenceFd The file descriptor of the Preference XML
|
|
* @param appFilesPath The full path to the app files directory
|
|
*/
|
|
private external fun executeApplication(romUri : String, romType : Int, romFd : Int, preferenceFd : Int, appFilesPath : String)
|
|
|
|
/**
|
|
* Terminate of all emulator threads and cause [emulationThread] to return
|
|
*/
|
|
private external fun stopEmulation()
|
|
|
|
/**
|
|
* This sets the surface object in libskyline to the provided value, emulation is halted if set to null
|
|
*
|
|
* @param surface The value to set surface to
|
|
*/
|
|
private external fun setSurface(surface : Surface?)
|
|
|
|
/**
|
|
* This returns the current FPS of the application
|
|
*/
|
|
private external fun getFps() : Int
|
|
|
|
/**
|
|
* This returns the current frame-time of the application
|
|
*/
|
|
private external fun getFrametime() : Float
|
|
|
|
/**
|
|
* This initializes a guest controller in libskyline
|
|
*
|
|
* @param index The arbitrary index of the controller, this is to handle matching with a partner Joy-Con
|
|
* @param type The type of the host controller
|
|
* @param partnerIndex The index of a partner Joy-Con if there is one
|
|
* @note This is blocking and will stall till input has been initialized on the guest
|
|
*/
|
|
private external fun setController(index : Int, type : Int, partnerIndex : Int = -1)
|
|
|
|
/**
|
|
* This flushes the controller updates on the guest
|
|
*
|
|
* @note This is blocking and will stall till input has been initialized on the guest
|
|
*/
|
|
private external fun updateControllers()
|
|
|
|
/**
|
|
* This sets the state of the buttons specified in the mask on a specific controller
|
|
*
|
|
* @param index The index of the controller this is directed to
|
|
* @param mask The mask of the button that are being set
|
|
* @param pressed If the buttons are being pressed or released
|
|
*/
|
|
private external fun setButtonState(index : Int, mask : Long, pressed : Boolean)
|
|
|
|
/**
|
|
* This sets the value of a specific axis on a specific controller
|
|
*
|
|
* @param index The index of the controller this is directed to
|
|
* @param axis The ID of the axis that is being modified
|
|
* @param value The value to set the axis to
|
|
*/
|
|
private external fun setAxisValue(index : Int, axis : Int, value : Int)
|
|
|
|
/**
|
|
* This sets the values of the points on the guest touch-screen
|
|
*
|
|
* @param points An array of skyline::input::TouchScreenPoint in C++ represented as integers
|
|
*/
|
|
private external fun setTouchState(points : IntArray)
|
|
|
|
/**
|
|
* This initializes all of the controllers from [InputManager] on the guest
|
|
*/
|
|
@Suppress("unused")
|
|
private fun initializeControllers() {
|
|
for (controller in InputManager.controllers.values) {
|
|
if (controller.type != ControllerType.None) {
|
|
val type = when (controller.type) {
|
|
ControllerType.None -> throw IllegalArgumentException()
|
|
ControllerType.HandheldProController -> if (settings.operationMode) ControllerType.ProController.id else ControllerType.HandheldProController.id
|
|
ControllerType.ProController, ControllerType.JoyConLeft, ControllerType.JoyConRight -> controller.type.id
|
|
}
|
|
|
|
val partnerIndex = when (controller) {
|
|
is JoyConLeftController -> controller.partnerId
|
|
is JoyConRightController -> controller.partnerId
|
|
else -> null
|
|
}
|
|
|
|
setController(controller.id, type, partnerIndex ?: -1)
|
|
}
|
|
}
|
|
|
|
updateControllers()
|
|
}
|
|
|
|
/**
|
|
* This executes the specified ROM
|
|
*
|
|
* @param rom The URI of the ROM to execute
|
|
*/
|
|
private fun executeApplication(rom : Uri) {
|
|
val romType = getRomFormat(rom, contentResolver).ordinal
|
|
val romFd = contentResolver.openFileDescriptor(rom, "r")!!
|
|
val preferenceFd = ParcelFileDescriptor.open(File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml"), ParcelFileDescriptor.MODE_READ_WRITE)
|
|
|
|
emulationThread = Thread {
|
|
executeApplication(rom.toString(), romType, romFd.detachFd(), preferenceFd.detachFd(), applicationContext.filesDir.canonicalPath + "/")
|
|
if (shouldFinish)
|
|
runOnUiThread { finish() }
|
|
}
|
|
|
|
emulationThread.start()
|
|
}
|
|
|
|
/**
|
|
* This makes the window fullscreen, sets up the performance statistics and finally calls [executeApplication] for executing the application
|
|
*/
|
|
@SuppressLint("SetTextI18n")
|
|
override fun onCreate(savedInstanceState : Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
setContentView(R.layout.emu_activity)
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
window.insetsController?.hide(WindowInsets.Type.navigationBars() or WindowInsets.Type.systemBars() or WindowInsets.Type.systemGestures() or WindowInsets.Type.statusBars())
|
|
window.insetsController?.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
} else {
|
|
@Suppress("DEPRECATION")
|
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
|
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
|
}
|
|
|
|
game_view.holder.addCallback(this)
|
|
|
|
if (settings.perfStats) {
|
|
perf_stats.postDelayed(object : Runnable {
|
|
override fun run() {
|
|
perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms"
|
|
perf_stats.postDelayed(this, 250)
|
|
}
|
|
}, 250)
|
|
}
|
|
|
|
@Suppress("DEPRECATION") val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) display!! else windowManager.defaultDisplay
|
|
display?.supportedModes?.maxBy { it.refreshRate + (it.physicalHeight * it.physicalWidth) }?.let { window.attributes.preferredDisplayModeId = it.modeId }
|
|
|
|
game_view.setOnTouchListener(this)
|
|
|
|
// Hide on screen controls when first controller is not set
|
|
on_screen_controller_view.isGone = !InputManager.controllers[0]!!.type.firstController || !settings.onScreenControl
|
|
on_screen_controller_view.setOnButtonStateChangedListener(::onButtonStateChanged)
|
|
on_screen_controller_view.setOnStickStateChangedListener(::onStickStateChanged)
|
|
|
|
on_screen_controller_toggle.isGone = on_screen_controller_toggle.isGone
|
|
on_screen_controller_toggle.setOnClickListener {
|
|
on_screen_controller_view.isInvisible = !on_screen_controller_view.isInvisible
|
|
}
|
|
|
|
executeApplication(intent.data!!)
|
|
}
|
|
|
|
/**
|
|
* This is used to stop the currently executing ROM and replace it with the one specified in the new intent
|
|
*/
|
|
override fun onNewIntent(intent : Intent?) {
|
|
super.onNewIntent(intent)
|
|
shouldFinish = false
|
|
|
|
stopEmulation()
|
|
emulationThread.join()
|
|
|
|
shouldFinish = true
|
|
|
|
executeApplication(intent?.data!!)
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
shouldFinish = false
|
|
|
|
stopEmulation()
|
|
emulationThread.join()
|
|
|
|
vibrators.forEach { (_, vibrator) -> vibrator.cancel() }
|
|
vibrators.clear()
|
|
}
|
|
|
|
/**
|
|
* This sets [surface] to [holder].surface and passes it into libskyline
|
|
*/
|
|
override fun surfaceCreated(holder : SurfaceHolder) {
|
|
Log.d(Tag, "surfaceCreated Holder: $holder")
|
|
surface = holder.surface
|
|
setSurface(surface)
|
|
}
|
|
|
|
/**
|
|
* This is purely used for debugging surface changes
|
|
*/
|
|
override fun surfaceChanged(holder : SurfaceHolder, format : Int, width : Int, height : Int) {
|
|
Log.d(Tag, "surfaceChanged Holder: $holder, Format: $format, Width: $width, Height: $height")
|
|
}
|
|
|
|
/**
|
|
* This sets [surface] to null and passes it into libskyline
|
|
*/
|
|
override fun surfaceDestroyed(holder : SurfaceHolder) {
|
|
Log.d(Tag, "surfaceDestroyed Holder: $holder")
|
|
surface = null
|
|
setSurface(surface)
|
|
}
|
|
|
|
/**
|
|
* This handles translating any [KeyHostEvent]s to a [GuestEvent] that is passed into libskyline
|
|
*/
|
|
override fun dispatchKeyEvent(event : KeyEvent) : Boolean {
|
|
if (event.repeatCount != 0)
|
|
return super.dispatchKeyEvent(event)
|
|
|
|
val action = when (event.action) {
|
|
KeyEvent.ACTION_DOWN -> ButtonState.Pressed
|
|
KeyEvent.ACTION_UP -> ButtonState.Released
|
|
else -> return super.dispatchKeyEvent(event)
|
|
}
|
|
|
|
return when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
|
is ButtonGuestEvent -> {
|
|
if (guestEvent.button != ButtonId.Menu)
|
|
setButtonState(guestEvent.id, guestEvent.button.value(), action.state)
|
|
true
|
|
}
|
|
|
|
is AxisGuestEvent -> {
|
|
setAxisValue(guestEvent.id, guestEvent.axis.ordinal, (if (action == ButtonState.Pressed) if (guestEvent.polarity) Short.MAX_VALUE else Short.MIN_VALUE else 0).toInt())
|
|
true
|
|
}
|
|
|
|
else -> super.dispatchKeyEvent(event)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The last value of the axes so the stagnant axes can be eliminated to not wastefully look them up
|
|
*/
|
|
private val axesHistory = arrayOfNulls<Float>(MotionHostEvent.axes.size)
|
|
|
|
/**
|
|
* The last value of the HAT axes so it can be ignored in [onGenericMotionEvent] so they are handled by [dispatchKeyEvent] instead
|
|
*/
|
|
private var oldHat = PointF()
|
|
|
|
/**
|
|
* This handles translating any [MotionHostEvent]s to a [GuestEvent] that is passed into libskyline
|
|
*/
|
|
override fun onGenericMotionEvent(event : MotionEvent) : Boolean {
|
|
if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE) {
|
|
val hat = PointF(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y))
|
|
|
|
if (hat == oldHat) {
|
|
for (axisItem in MotionHostEvent.axes.withIndex()) {
|
|
val axis = axisItem.value
|
|
var value = event.getAxisValue(axis)
|
|
|
|
if ((event.historySize != 0 && value != event.getHistoricalAxisValue(axis, 0)) || (axesHistory[axisItem.index]?.let { it == value } == false)) {
|
|
var polarity = value >= 0
|
|
|
|
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
|
|
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
|
polarity = false
|
|
InputManager.eventMap[hostEvent.copy(polarity = false)]
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
|
|
when (guestEvent) {
|
|
is ButtonGuestEvent -> {
|
|
if (guestEvent.button != ButtonId.Menu)
|
|
setButtonState(guestEvent.id, guestEvent.button.value(), if (abs(value) >= guestEvent.threshold) ButtonState.Pressed.state else ButtonState.Released.state)
|
|
}
|
|
|
|
is AxisGuestEvent -> {
|
|
value = guestEvent.value(value)
|
|
value = if (polarity) abs(value) else -abs(value)
|
|
value = if (guestEvent.axis == AxisId.LX || guestEvent.axis == AxisId.RX) value else -value // TODO: Test this
|
|
|
|
setAxisValue(guestEvent.id, guestEvent.axis.ordinal, (value * Short.MAX_VALUE).toInt())
|
|
}
|
|
}
|
|
}
|
|
|
|
axesHistory[axisItem.index] = value
|
|
}
|
|
|
|
return true
|
|
} else {
|
|
oldHat = hat
|
|
}
|
|
}
|
|
|
|
return super.onGenericMotionEvent(event)
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
override fun onTouch(view : View, event : MotionEvent) : Boolean {
|
|
val count = if (event.action != MotionEvent.ACTION_UP && event.action != MotionEvent.ACTION_CANCEL) event.pointerCount else 0
|
|
val points = IntArray(count * 5) // This is an array of skyline::input::TouchScreenPoint in C++ as that allows for efficient transfer of values to it
|
|
var offset = 0
|
|
for (index in 0 until count) {
|
|
val pointer = MotionEvent.PointerCoords()
|
|
event.getPointerCoords(index, pointer)
|
|
|
|
val x = 0f.coerceAtLeast(pointer.x * 1280 / view.width).toInt()
|
|
val y = 0f.coerceAtLeast(pointer.y * 720 / view.height).toInt()
|
|
|
|
points[offset++] = x
|
|
points[offset++] = y
|
|
points[offset++] = pointer.touchMinor.toInt()
|
|
points[offset++] = pointer.touchMajor.toInt()
|
|
points[offset++] = (pointer.orientation * 180 / Math.PI).toInt()
|
|
}
|
|
|
|
setTouchState(points)
|
|
|
|
return true
|
|
}
|
|
|
|
private fun onButtonStateChanged(buttonId : ButtonId, state : ButtonState) = setButtonState(0, buttonId.value(), state.state)
|
|
|
|
private fun onStickStateChanged(buttonId : ButtonId, position : PointF) {
|
|
val stickId = when (buttonId) {
|
|
ButtonId.LeftStick -> StickId.Left
|
|
|
|
ButtonId.RightStick -> StickId.Right
|
|
|
|
else -> error("Invalid button id")
|
|
}
|
|
setAxisValue(0, stickId.xAxis.ordinal, (position.x * Short.MAX_VALUE).toInt())
|
|
setAxisValue(0, stickId.yAxis.ordinal, (-position.y * Short.MAX_VALUE).toInt()) // Y is inverted, since drawing starts from top left
|
|
}
|
|
|
|
@SuppressLint("WrongConstant")
|
|
@Suppress("unused")
|
|
fun vibrateDevice(index : Int, timing : LongArray, amplitude : IntArray) {
|
|
val vibrator = if (vibrators[index] != null) {
|
|
vibrators[index]!!
|
|
} else {
|
|
InputManager.controllers[index]!!.rumbleDeviceDescriptor?.let {
|
|
if (it == "builtin") {
|
|
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
vibrators[index] = vibrator
|
|
vibrator
|
|
} else {
|
|
for (id in InputDevice.getDeviceIds()) {
|
|
val device = InputDevice.getDevice(id)
|
|
if (device.descriptor == InputManager.controllers[index]!!.rumbleDeviceDescriptor) {
|
|
vibrators[index] = device.vibrator
|
|
device.vibrator
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
val effect = VibrationEffect.createWaveform(timing, amplitude, 0)
|
|
vibrator.vibrate(effect)
|
|
}
|
|
|
|
@Suppress("unused")
|
|
fun clearVibrationDevice(index : Int) {
|
|
vibrators[index]?.cancel()
|
|
}
|
|
}
|