2020-03-27 20:36:02 +01:00
|
|
|
/*
|
2020-04-19 23:04:05 +02:00
|
|
|
* SPDX-License-Identifier: MPL-2.0
|
2020-03-27 20:36:02 +01:00
|
|
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
|
|
|
*/
|
|
|
|
|
2019-12-02 14:39:08 +01:00
|
|
|
package emu.skyline
|
|
|
|
|
2020-04-22 19:02:27 +02:00
|
|
|
import android.annotation.SuppressLint
|
2020-02-11 07:34:22 +01:00
|
|
|
import android.content.Intent
|
2019-12-02 14:39:08 +01:00
|
|
|
import android.net.Uri
|
2020-07-06 22:47:23 +02:00
|
|
|
import android.os.Build
|
2019-12-02 14:39:08 +01:00
|
|
|
import android.os.Bundle
|
|
|
|
import android.os.ParcelFileDescriptor
|
|
|
|
import android.util.Log
|
2020-07-06 22:47:23 +02:00
|
|
|
import android.view.*
|
2019-12-02 14:39:08 +01:00
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
2020-04-18 02:16:09 +02:00
|
|
|
import androidx.preference.PreferenceManager
|
2020-05-28 21:27:25 +02:00
|
|
|
import emu.skyline.input.AxisId
|
|
|
|
import emu.skyline.input.ButtonId
|
2020-04-26 16:32:24 +02:00
|
|
|
import emu.skyline.input.ButtonState
|
2020-04-03 13:47:32 +02:00
|
|
|
import emu.skyline.loader.getRomFormat
|
2020-04-18 02:16:09 +02:00
|
|
|
import kotlinx.android.synthetic.main.emu_activity.*
|
2019-12-02 14:39:08 +01:00
|
|
|
import java.io.File
|
2020-04-26 16:32:24 +02:00
|
|
|
import kotlin.math.abs
|
2019-12-02 14:39:08 +01:00
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback {
|
2019-12-02 14:39:08 +01:00
|
|
|
init {
|
|
|
|
System.loadLibrary("skyline") // libskyline.so
|
|
|
|
}
|
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
|
|
|
* The file descriptor of the ROM
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private lateinit var romFd : ParcelFileDescriptor
|
2020-04-03 13:47:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The file descriptor of the application Preference XML
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private lateinit var preferenceFd : ParcelFileDescriptor
|
2020-04-03 13:47:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The surface object used for displaying frames
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private var surface : Surface? = null
|
2020-04-03 13:47:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A boolean flag denoting if the emulation thread should call finish() or not
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private var shouldFinish : Boolean = true
|
2019-12-02 14:39:08 +01:00
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
|
|
|
* The Kotlin thread on which emulation code executes
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private lateinit var emulationThread : Thread
|
2020-04-03 13:47:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2020-08-08 21:38:51 +02:00
|
|
|
* @param appFilesPath The full path to the app files directory
|
2020-04-03 13:47:32 +02:00
|
|
|
*/
|
2020-08-08 21:38:51 +02:00
|
|
|
private external fun executeApplication(romUri : String, romType : Int, romFd : Int, preferenceFd : Int, appFilesPath : String)
|
2020-04-03 13:47:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This sets the halt flag in libskyline to the provided value, if set to true it causes libskyline to halt emulation
|
|
|
|
*
|
|
|
|
* @param halt The value to set halt to
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private external fun setHalt(halt : Boolean)
|
2020-04-03 13:47:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private external fun setSurface(surface : Surface?)
|
2019-12-02 14:39:08 +01:00
|
|
|
|
2020-04-18 02:16:09 +02:00
|
|
|
/**
|
|
|
|
* This returns the current FPS of the application
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private external fun getFps() : Int
|
2020-04-18 02:16:09 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This returns the current frame-time of the application
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private external fun getFrametime() : Float
|
2020-04-18 02:16:09 +02:00
|
|
|
|
2020-04-26 16:32:24 +02:00
|
|
|
/**
|
|
|
|
* This sets the state of a specific button
|
|
|
|
*/
|
2020-04-26 01:34:35 +02:00
|
|
|
private external fun setButtonState(id : Long, state : Int)
|
|
|
|
|
2020-04-26 16:32:24 +02:00
|
|
|
/**
|
|
|
|
* This sets the value of a specific axis
|
|
|
|
*/
|
2020-04-26 01:34:35 +02:00
|
|
|
private external fun setAxisValue(id : Int, value : Int)
|
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
2020-08-08 21:38:51 +02:00
|
|
|
* This executes the specified ROM, [preferenceFd] is assumed to be valid beforehand
|
2020-04-03 13:47:32 +02:00
|
|
|
*
|
|
|
|
* @param rom The URI of the ROM to execute
|
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
private fun executeApplication(rom : Uri) {
|
2020-04-03 13:47:32 +02:00
|
|
|
val romType = getRomFormat(rom, contentResolver).ordinal
|
2020-02-11 07:34:22 +01:00
|
|
|
romFd = contentResolver.openFileDescriptor(rom, "r")!!
|
2020-04-03 13:47:32 +02:00
|
|
|
|
|
|
|
emulationThread = Thread {
|
2020-02-11 07:34:22 +01:00
|
|
|
while ((surface == null))
|
|
|
|
Thread.yield()
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-08-08 21:38:51 +02:00
|
|
|
executeApplication(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, applicationContext.filesDir.canonicalPath + "/")
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-02-11 07:34:22 +01:00
|
|
|
if (shouldFinish)
|
|
|
|
runOnUiThread { finish() }
|
|
|
|
}
|
2020-04-03 13:47:32 +02:00
|
|
|
|
|
|
|
emulationThread.start()
|
2020-02-11 07:34:22 +01:00
|
|
|
}
|
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
2020-08-08 21:38:51 +02:00
|
|
|
* This makes the window fullscreen then sets up [preferenceFd], sets up the performance statistics and finally calls [executeApplication] for executing the application
|
2020-04-03 13:47:32 +02:00
|
|
|
*/
|
2020-04-22 19:02:27 +02:00
|
|
|
@SuppressLint("SetTextI18n")
|
2020-04-24 13:39:13 +02:00
|
|
|
override fun onCreate(savedInstanceState : Bundle?) {
|
2019-12-02 14:39:08 +01:00
|
|
|
super.onCreate(savedInstanceState)
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-04-18 02:16:09 +02:00
|
|
|
setContentView(R.layout.emu_activity)
|
|
|
|
|
2020-07-06 22:47:23 +02:00
|
|
|
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
|
|
|
|
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)
|
|
|
|
}
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2019-12-02 14:39:08 +01:00
|
|
|
val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml")
|
|
|
|
preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE)
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2019-12-02 14:39:08 +01:00
|
|
|
game_view.holder.addCallback(this)
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-04-18 02:16:09 +02:00
|
|
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
|
|
|
|
|
|
if (sharedPreferences.getBoolean("perf_stats", false)) {
|
2020-04-24 13:39:13 +02:00
|
|
|
lateinit var perfRunnable : Runnable
|
2020-04-18 02:16:09 +02:00
|
|
|
|
|
|
|
perfRunnable = Runnable {
|
|
|
|
perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms"
|
|
|
|
perf_stats.postDelayed(perfRunnable, 250)
|
|
|
|
}
|
|
|
|
|
|
|
|
perf_stats.postDelayed(perfRunnable, 250)
|
|
|
|
}
|
|
|
|
|
2020-04-12 22:29:19 +02:00
|
|
|
executeApplication(intent.data!!)
|
2020-02-11 07:34:22 +01:00
|
|
|
}
|
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
2020-04-12 22:29:19 +02:00
|
|
|
* This is used to stop the currently executing ROM and replace it with the one specified in the new intent
|
2020-04-03 13:47:32 +02:00
|
|
|
*/
|
2020-04-24 13:39:13 +02:00
|
|
|
override fun onNewIntent(intent : Intent?) {
|
2020-02-11 07:34:22 +01:00
|
|
|
shouldFinish = false
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-02-11 07:34:22 +01:00
|
|
|
setHalt(true)
|
2020-04-03 13:47:32 +02:00
|
|
|
emulationThread.join()
|
|
|
|
|
2020-02-11 07:34:22 +01:00
|
|
|
shouldFinish = true
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-02-11 07:34:22 +01:00
|
|
|
romFd.close()
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-04-12 22:29:19 +02:00
|
|
|
executeApplication(intent?.data!!)
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-02-11 07:34:22 +01:00
|
|
|
super.onNewIntent(intent)
|
2019-12-02 14:39:08 +01:00
|
|
|
}
|
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
2020-04-12 22:29:19 +02:00
|
|
|
* This is used to halt emulation entirely
|
2020-04-03 13:47:32 +02:00
|
|
|
*/
|
2019-12-02 14:39:08 +01:00
|
|
|
override fun onDestroy() {
|
2020-02-11 07:34:22 +01:00
|
|
|
shouldFinish = false
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2019-12-26 19:10:29 +01:00
|
|
|
setHalt(true)
|
2020-04-03 13:47:32 +02:00
|
|
|
emulationThread.join()
|
|
|
|
|
2019-12-02 14:39:08 +01:00
|
|
|
romFd.close()
|
|
|
|
preferenceFd.close()
|
2020-04-03 13:47:32 +02:00
|
|
|
|
2020-02-11 07:34:22 +01:00
|
|
|
super.onDestroy()
|
2019-12-02 14:39:08 +01:00
|
|
|
}
|
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
2020-04-12 22:29:19 +02:00
|
|
|
* This sets [surface] to [holder].surface and passes it into libskyline
|
2020-04-03 13:47:32 +02:00
|
|
|
*/
|
2020-06-15 17:37:28 +02:00
|
|
|
override fun surfaceCreated(holder : SurfaceHolder) {
|
2020-04-30 23:53:45 +02:00
|
|
|
Log.d("surfaceCreated", "Holder: $holder")
|
2020-06-15 17:37:28 +02:00
|
|
|
surface = holder.surface
|
2019-12-26 19:10:29 +01:00
|
|
|
setSurface(surface)
|
2019-12-02 14:39:08 +01:00
|
|
|
}
|
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
2020-04-12 22:29:19 +02:00
|
|
|
* This is purely used for debugging surface changes
|
2020-04-03 13:47:32 +02:00
|
|
|
*/
|
2020-06-15 17:37:28 +02:00
|
|
|
override fun surfaceChanged(holder : SurfaceHolder, format : Int, width : Int, height : Int) {
|
2020-04-30 23:53:45 +02:00
|
|
|
Log.d("surfaceChanged", "Holder: $holder, Format: $format, Width: $width, Height: $height")
|
2019-12-02 14:39:08 +01:00
|
|
|
}
|
|
|
|
|
2020-04-03 13:47:32 +02:00
|
|
|
/**
|
2020-04-12 22:29:19 +02:00
|
|
|
* This sets [surface] to null and passes it into libskyline
|
2020-04-03 13:47:32 +02:00
|
|
|
*/
|
2020-06-15 17:37:28 +02:00
|
|
|
override fun surfaceDestroyed(holder : SurfaceHolder) {
|
2020-04-30 23:53:45 +02:00
|
|
|
Log.d("surfaceDestroyed", "Holder: $holder")
|
2019-12-02 14:39:08 +01:00
|
|
|
surface = null
|
2019-12-26 19:10:29 +01:00
|
|
|
setSurface(surface)
|
2019-12-02 14:39:08 +01:00
|
|
|
}
|
2020-04-26 16:32:24 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* This handles passing on any key events to libskyline
|
|
|
|
*/
|
|
|
|
override fun dispatchKeyEvent(event : KeyEvent) : Boolean {
|
|
|
|
val action : ButtonState = when (event.action) {
|
|
|
|
KeyEvent.ACTION_DOWN -> ButtonState.Pressed
|
|
|
|
KeyEvent.ACTION_UP -> ButtonState.Released
|
|
|
|
else -> return false
|
|
|
|
}
|
|
|
|
|
2020-05-28 21:27:25 +02:00
|
|
|
val buttonMap : Map<Int, ButtonId> = mapOf(
|
|
|
|
KeyEvent.KEYCODE_BUTTON_A to ButtonId.A,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_B to ButtonId.B,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_X to ButtonId.X,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_Y to ButtonId.Y,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_THUMBL to ButtonId.LeftStick,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_THUMBR to ButtonId.RightStick,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_L1 to ButtonId.L,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_R1 to ButtonId.R,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_L2 to ButtonId.ZL,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_R2 to ButtonId.ZR,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_START to ButtonId.Plus,
|
|
|
|
KeyEvent.KEYCODE_BUTTON_SELECT to ButtonId.Minus,
|
|
|
|
KeyEvent.KEYCODE_DPAD_DOWN to ButtonId.DpadDown,
|
|
|
|
KeyEvent.KEYCODE_DPAD_UP to ButtonId.DpadUp,
|
|
|
|
KeyEvent.KEYCODE_DPAD_LEFT to ButtonId.DpadLeft,
|
|
|
|
KeyEvent.KEYCODE_DPAD_RIGHT to ButtonId.DpadRight)
|
2020-04-26 16:32:24 +02:00
|
|
|
|
|
|
|
return try {
|
2020-05-28 21:27:25 +02:00
|
|
|
setButtonState(buttonMap.getValue(event.keyCode).value(), action.ordinal)
|
2020-04-26 16:32:24 +02:00
|
|
|
true
|
|
|
|
} catch (ignored : NoSuchElementException) {
|
|
|
|
super.dispatchKeyEvent(event)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is the controller HAT X value
|
|
|
|
*/
|
|
|
|
private var controllerHatX : Float = 0.0f
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is the controller HAT Y value
|
|
|
|
*/
|
|
|
|
private var controllerHatY : Float = 0.0f
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This handles passing on any motion events to libskyline
|
|
|
|
*/
|
|
|
|
override fun dispatchGenericMotionEvent(event : MotionEvent) : Boolean {
|
|
|
|
if ((event.source and InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD ||
|
|
|
|
(event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) {
|
2020-05-28 21:27:25 +02:00
|
|
|
val hatXMap : Map<Float, ButtonId> = mapOf(
|
|
|
|
-1.0f to ButtonId.DpadLeft,
|
|
|
|
+1.0f to ButtonId.DpadRight)
|
2020-04-26 16:32:24 +02:00
|
|
|
|
2020-05-28 21:27:25 +02:00
|
|
|
val hatYMap : Map<Float, ButtonId> = mapOf(
|
|
|
|
-1.0f to ButtonId.DpadUp,
|
|
|
|
+1.0f to ButtonId.DpadDown)
|
2020-04-26 16:32:24 +02:00
|
|
|
|
|
|
|
if (controllerHatX != event.getAxisValue(MotionEvent.AXIS_HAT_X)) {
|
|
|
|
if (event.getAxisValue(MotionEvent.AXIS_HAT_X) == 0.0f)
|
|
|
|
setButtonState(hatXMap.getValue(controllerHatX).value(), ButtonState.Released.ordinal)
|
|
|
|
else
|
|
|
|
setButtonState(hatXMap.getValue(event.getAxisValue(MotionEvent.AXIS_HAT_X)).value(), ButtonState.Pressed.ordinal)
|
|
|
|
|
|
|
|
controllerHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X)
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
if (controllerHatY != event.getAxisValue(MotionEvent.AXIS_HAT_Y)) {
|
|
|
|
if (event.getAxisValue(MotionEvent.AXIS_HAT_Y) == 0.0f)
|
|
|
|
setButtonState(hatYMap.getValue(controllerHatY).value(), ButtonState.Released.ordinal)
|
|
|
|
else
|
|
|
|
setButtonState(hatYMap.getValue(event.getAxisValue(MotionEvent.AXIS_HAT_Y)).value(), ButtonState.Pressed.ordinal)
|
|
|
|
|
|
|
|
controllerHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y)
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.action == MotionEvent.ACTION_MOVE) {
|
2020-05-28 21:27:25 +02:00
|
|
|
val axisMap : Map<Int, AxisId> = mapOf(
|
|
|
|
MotionEvent.AXIS_X to AxisId.LX,
|
|
|
|
MotionEvent.AXIS_Y to AxisId.LY,
|
|
|
|
MotionEvent.AXIS_Z to AxisId.RX,
|
|
|
|
MotionEvent.AXIS_RZ to AxisId.RY)
|
2020-04-26 16:32:24 +02:00
|
|
|
|
|
|
|
//TODO: Digital inputs based off of analog sticks
|
|
|
|
event.device.motionRanges.forEach {
|
|
|
|
if (axisMap.containsKey(it.axis)) {
|
|
|
|
var axisValue : Float = event.getAxisValue(it.axis)
|
|
|
|
if (abs(axisValue) <= it.flat)
|
|
|
|
axisValue = 0.0f
|
|
|
|
|
|
|
|
val ratio : Float = axisValue / (it.max - it.min)
|
|
|
|
val rangedAxisValue : Int = (ratio * (Short.MAX_VALUE - Short.MIN_VALUE)).toInt()
|
|
|
|
|
|
|
|
setAxisValue(axisMap.getValue(it.axis).ordinal, rangedAxisValue)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return super.dispatchGenericMotionEvent(event)
|
|
|
|
}
|
2019-12-02 14:39:08 +01:00
|
|
|
}
|