/* * 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.Intent import android.net.Uri import android.os.Build import android.os.Bundle import android.os.ParcelFileDescriptor import android.util.Log import android.view.* import androidx.appcompat.app.AppCompatActivity import androidx.preference.PreferenceManager import emu.skyline.input.AxisId import emu.skyline.input.ButtonId import emu.skyline.input.ButtonState import emu.skyline.loader.getRomFormat import kotlinx.android.synthetic.main.emu_activity.* import java.io.File import kotlin.math.abs class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback { init { System.loadLibrary("skyline") // libskyline.so } /** * The file descriptor of the ROM */ private lateinit var romFd : ParcelFileDescriptor /** * The file descriptor of the application Preference XML */ private lateinit var preferenceFd : ParcelFileDescriptor /** * The surface object used for displaying frames */ private var surface : Surface? = null /** * A boolean flag denoting if the emulation thread should call finish() or not */ private var shouldFinish : Boolean = true /** * The Kotlin thread on which emulation code executes */ private lateinit var emulationThread : Thread /** * 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) /** * 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 */ private external fun setHalt(halt : Boolean) /** * 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 sets the state of a specific button */ private external fun setButtonState(id : Long, state : Int) /** * This sets the value of a specific axis */ private external fun setAxisValue(id : Int, value : Int) /** * This executes the specified ROM, [preferenceFd] is assumed to be valid beforehand * * @param rom The URI of the ROM to execute */ private fun executeApplication(rom : Uri) { val romType = getRomFormat(rom, contentResolver).ordinal romFd = contentResolver.openFileDescriptor(rom, "r")!! emulationThread = Thread { while ((surface == null)) Thread.yield() executeApplication(Uri.decode(rom.toString()), romType, romFd.fd, preferenceFd.fd, applicationContext.filesDir.canonicalPath + "/") if (shouldFinish) runOnUiThread { finish() } } emulationThread.start() } /** * This makes the window fullscreen then sets up [preferenceFd], 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 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) } val preference = File("${applicationInfo.dataDir}/shared_prefs/${applicationInfo.packageName}_preferences.xml") preferenceFd = ParcelFileDescriptor.open(preference, ParcelFileDescriptor.MODE_READ_WRITE) game_view.holder.addCallback(this) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) if (sharedPreferences.getBoolean("perf_stats", false)) { lateinit var perfRunnable : Runnable perfRunnable = Runnable { perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms" perf_stats.postDelayed(perfRunnable, 250) } perf_stats.postDelayed(perfRunnable, 250) } 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?) { shouldFinish = false setHalt(true) emulationThread.join() shouldFinish = true romFd.close() executeApplication(intent?.data!!) super.onNewIntent(intent) } /** * This is used to halt emulation entirely */ override fun onDestroy() { shouldFinish = false setHalt(true) emulationThread.join() romFd.close() preferenceFd.close() super.onDestroy() } /** * This sets [surface] to [holder].surface and passes it into libskyline */ override fun surfaceCreated(holder : SurfaceHolder) { Log.d("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("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("surfaceDestroyed", "Holder: $holder") surface = null setSurface(surface) } /** * 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 } val buttonMap : Map = 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) return try { setButtonState(buttonMap.getValue(event.keyCode).value(), action.ordinal) 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) { val hatXMap : Map = mapOf( -1.0f to ButtonId.DpadLeft, +1.0f to ButtonId.DpadRight) val hatYMap : Map = mapOf( -1.0f to ButtonId.DpadUp, +1.0f to ButtonId.DpadDown) 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) { val axisMap : Map = 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) //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) } }