diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt index a4d9bac35a..6c4765bf32 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.kt @@ -363,11 +363,11 @@ object NativeLibrary { @JvmStatic external fun StopEmulation() - /** - * Ensures that IsRunning will return true from now on until emulation exits. - * (If this is not called, IsRunning will start returning true at some point - * after calling Run.) - */ + /** + * Ensures that IsRunning will return true from now on until emulation exits. + * (If this is not called, IsRunning will start returning true at some point + * after calling Run.) + */ @JvmStatic external fun SetIsBooting() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index 8a1d8addbd..5aee5ebe9e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.activities +import android.content.Context import android.content.DialogInterface import android.content.Intent import android.graphics.Rect @@ -14,13 +15,17 @@ import android.util.SparseIntArray import android.view.KeyEvent import android.view.MenuItem import android.view.MotionEvent +import android.view.ScaleGestureDetector import android.view.View +import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams import android.view.WindowManager +import android.widget.FrameLayout import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu +import androidx.core.view.doOnLayout import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -36,12 +41,16 @@ import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.databinding.ActivityEmulationBinding import org.dolphinemu.dolphinemu.databinding.DialogInputAdjustBinding import org.dolphinemu.dolphinemu.databinding.DialogNfcFiguresManagerBinding +import org.dolphinemu.dolphinemu.features.gba.GbaLibrary +import org.dolphinemu.dolphinemu.features.gba.GbaOverlayView +import org.dolphinemu.dolphinemu.features.gba.GbaRenderManager import org.dolphinemu.dolphinemu.features.infinitybase.InfinityConfig import org.dolphinemu.dolphinemu.features.infinitybase.model.Figure import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlot import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlotAdapter import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener +import org.dolphinemu.dolphinemu.features.input.model.InputOverrider import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.Settings @@ -72,6 +81,12 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { private lateinit var settings: Settings + private val gbaViews = mutableListOf() + + private val lastGbaTapTimes = mutableMapOf() + + private var isGbaLocked = false + override var themeId = 0 private var menuVisible = false @@ -186,6 +201,12 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { super.onCreate(savedInstanceState) + // gba overlay setup, also clean up views from previous launch to prevent stacking + GbaRenderManager.detach() + gbaViews.forEach { binding.root.removeView(it) } + gbaViews.clear() + lastGbaTapTimes.clear() + MainPresenter.skipRescanningLibrary() if (savedInstanceState == null) { @@ -211,6 +232,38 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { binding = ActivityEmulationBinding.inflate(layoutInflater) setContentView(binding.root) + //Read snap state before creating new views + val globalGbaPrefs = getSharedPreferences("gba_overlay", Context.MODE_PRIVATE) + isGbaLocked = globalGbaPrefs.getBoolean("gba_locked", false) + + for (slot in 0 until 4) { + if (IntSetting.getSettingForSIDevice(slot).int != InputOverlay.EMULATED_GBA_CONTROLLER) continue + val view = GbaOverlayView(this) + view.gbaSlot = slot + val slotPrefs = getSharedPreferences("gba_overlay_${'$'}slot", Context.MODE_PRIVATE) + val sw = slotPrefs.getFloat("gba_width", 480f).coerceIn(120f, 960f) + val sh = slotPrefs.getFloat("gba_height", 320f).coerceIn(80f, 640f) + val screenW = resources.displayMetrics.widthPixels.toFloat() + val screenH = resources.displayMetrics.heightPixels.toFloat() + var sx = slotPrefs.getFloat("gba_x", 16f + slot * 20f) + var sy = slotPrefs.getFloat("gba_y", screenH - sh - 16f - slot * 20f) + if (sx < 0 || sx > screenW) sx = 16f + slot * 20f + if (sy < 0 || sy > screenH) sy = screenH - sh - 16f + val params = FrameLayout.LayoutParams(sw.toInt(), sh.toInt()) + binding.root.addView(view, 0, params) + view.x = sx; + view.y = sy + view.visibility = android.view.View.VISIBLE + InputOverrider.registerGBA(slot) + attachGbaTouchListener(view, slot, slotPrefs) + gbaViews.add(view) + } + + if (gbaViews.isNotEmpty() && NativeLibrary.IsGameMetadataValid()) { + GbaRenderManager.attach(gbaViews) + binding.root.doOnLayout { applyGbaLayout() } + } + setInsets() // Find or create the EmulationFragment @@ -332,6 +385,25 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { emulationFragment?.refreshInputOverlay() updateDisplaySettings() + + val activeSlots = (0 until 4).filter { + IntSetting.getSettingForSIDevice(it).int == InputOverlay.EMULATED_GBA_CONTROLLER + } + gbaViews.forEachIndexed { index, view -> + if (index < activeSlots.size) { + view.gbaSlot = activeSlots[index] + view.visibility = android.view.View.VISIBLE + InputOverrider.registerGBA(activeSlots[index]) + } else view.visibility = android.view.View.GONE + } + if (gbaViews.isNotEmpty()) { + if (GbaRenderManager.isAttached()) { + GbaRenderManager.updateViews(gbaViews) + } else { + GbaRenderManager.attach(gbaViews) + } + binding.root.post { applyGbaLayout() } + } } catch (_: IllegalStateException) { // Most likely the core delivered an onTitleChanged while emulation was shutting down. // Let's just ignore it, since we're about to shut down anyway. @@ -340,9 +412,41 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { override fun onDestroy() { super.onDestroy() + GbaRenderManager.detach() + for (slot in 0 until 4) { + InputOverrider.unregisterGBA(slot) + } + gbaViews.forEach { binding.root.removeView(it) } + gbaViews.clear() settings.close() } + override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { + super.onConfigurationChanged(newConfig) + if (gbaViews.isNotEmpty()) { + GbaRenderManager.updateViews(gbaViews) + binding.root.post { applyGbaLayout() } + + } else { + //no gba - restore game to full screen to try stop portrait squish into landscape + binding.frameEmulationFragment.x = 0f + binding.frameEmulationFragment.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + binding.frameEmulationFragment.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + binding.frameEmulationFragment.requestLayout() + } + } + + private fun setGbaViewsTouchable(touchable: Boolean) { + gbaViews.forEach { view -> + view.isClickable = touchable + view.isFocusable = touchable + view.isFocusableInTouchMode = touchable + if (!touchable) { + view.setOnTouchListener(null) + } + } + } + override fun onBackPressed() { if (!closeSubmenu()) { toggleMenu() @@ -361,7 +465,8 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { WindowCompat.setDecorFitsSystemWindows(window, false) WindowInsetsControllerCompat(window, window.decorView).let { controller -> controller.hide(WindowInsetsCompat.Type.systemBars()) - controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } } @@ -443,6 +548,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { menu.findItem(R.id.menu_emulation_ir_recenter).isChecked = BooleanSetting.MAIN_IR_ALWAYS_RECENTER.boolean } + menu.findItem(R.id.menu_emulation_gba_snap)?.isChecked = isGbaLocked popup.setOnMenuItemClickListener { item: MenuItem -> onOptionsItemSelected(item) } popup.show() } @@ -471,6 +577,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { item.isChecked = !item.isChecked toggleRecenter(item.isChecked) } + + MENU_ACTION_GBA_SNAP -> { + item.isChecked = !item.isChecked + toggleGBASnap() + } } } @@ -518,6 +629,14 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { MENU_ACTION_SKYLANDERS -> showSkylanderPortalSettings() MENU_ACTION_INFINITY_BASE -> showInfinityBaseSettings() MENU_ACTION_EXIT -> emulationFragment!!.stopEmulation() + MENU_ACTION_GBA_SNAP -> toggleGBASnap() + MENU_ACTION_GBA_RESET -> { + isGbaLocked = false; + resetGBAScreens(); + binding.root.post { applyGbaLayout() } + } + + MENU_ACTION_GBA_RESET_CORE -> resetGbaCore() } } @@ -533,9 +652,21 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { private fun editControlsPlacement() { if (emulationFragment!!.isConfiguringControls) { emulationFragment?.stopConfiguringControls() + setGbaViewsTouchable(true) + if (!isGbaLocked) { + gbaViews.forEachIndexed { _, view -> + val slot = view.gbaSlot + val slotPrefs = getSharedPreferences( + "gba_overlay_${'$'}slot", + Context.MODE_PRIVATE + ) + attachGbaTouchListener(view, slot, slotPrefs) + } + } } else { closeSubmenu() closeMenu() + setGbaViewsTouchable(false) emulationFragment?.startConfiguringControls() } } @@ -571,12 +702,14 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { emulationFragment?.refreshInputOverlay() } } + InputOverlay.OVERLAY_WIIMOTE_CLASSIC -> { val wiiClassicLatchingButtons = BooleanArray(11) val classicSettingBase = "MAIN_BUTTON_LATCHING_CLASSIC_" for (i in wiiClassicLatchingButtons.indices) { - wiiClassicLatchingButtons[i] = BooleanSetting.valueOf(classicSettingBase + i).boolean + wiiClassicLatchingButtons[i] = + BooleanSetting.valueOf(classicSettingBase + i).boolean } builder.setMultiChoiceItems( R.array.classicLatchableButtons, wiiClassicLatchingButtons @@ -586,6 +719,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { emulationFragment?.refreshInputOverlay() } } + InputOverlay.OVERLAY_WIIMOTE_NUNCHUK -> { val nunchukLatchingButtons = BooleanArray(9) val nunchukSettingBase = "MAIN_BUTTON_LATCHING_WII_" @@ -603,21 +737,27 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { builder.setMultiChoiceItems( R.array.nunchukLatchableButtons, nunchukLatchingButtons ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> - BooleanSetting.valueOf(nunchukSettingBase + translateToSettingsIndex(indexSelected)) + BooleanSetting.valueOf( + nunchukSettingBase + translateToSettingsIndex( + indexSelected + ) + ) .setBoolean(settings, isChecked) emulationFragment?.refreshInputOverlay() } } + else -> { val wiimoteLatchingButtons = BooleanArray(7) val wiimoteSettingBase = "MAIN_BUTTON_LATCHING_WII_" for (i in wiimoteLatchingButtons.indices) { - wiimoteLatchingButtons[i] = BooleanSetting.valueOf(wiimoteSettingBase + i).boolean + wiimoteLatchingButtons[i] = + BooleanSetting.valueOf(wiimoteSettingBase + i).boolean } builder.setMultiChoiceItems( - R.array.wiimoteLatchableButtons, wiimoteLatchingButtons + R.array.wiimoteLatchableButtons, wiimoteLatchingButtons ) { _: DialogInterface?, indexSelected: Int, isChecked: Boolean -> BooleanSetting.valueOf(wiimoteSettingBase + indexSelected) .setBoolean(settings, isChecked) @@ -932,7 +1072,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { when (position) { 0 -> infinityFigures[position].label = getString(R.string.infinity_hexagon_label) 1 -> infinityFigures[position].label = getString(R.string.infinity_power_hex_two_label) - 2 -> infinityFigures[position].label = getString(R.string.infinity_power_hex_three_label) + 2 -> infinityFigures[position].label = + getString(R.string.infinity_power_hex_three_label) + 3 -> infinityFigures[position].label = getString(R.string.infinity_p1_label) 4 -> infinityFigures[position].label = getString(R.string.infinity_p1a1_label) 5 -> infinityFigures[position].label = getString(R.string.infinity_p1a2_label) @@ -975,6 +1117,22 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { if (anyMenuClosed) return true } + + if (!isGbaLocked && gbaViews.isNotEmpty()) { + val loc = IntArray(2) + for (gbaView in gbaViews) { + gbaView.getLocationOnScreen(loc) + val bounds = android.graphics.Rect( + loc[0], + loc[1], + loc[0] + gbaView.width, + loc[1] + gbaView.height + ) + if (bounds.contains(event.rawX.toInt(), event.rawY.toInt())) { + return gbaView.dispatchTouchEvent(event) + } + } + } return super.dispatchTouchEvent(event) } @@ -1016,6 +1174,246 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { this.themeId = themeId } + // gba touch for scale/drag listener while in unlocked gba mode. + private fun attachGbaTouchListener( + view: GbaOverlayView, + slot: Int, + slotPrefs: android.content.SharedPreferences + ) { + var dragX = 0f + var dragY = 0f + + // Initial dimensions for scaling math + val params = view.layoutParams as FrameLayout.LayoutParams + var cw = params.width.toFloat() + var ch = params.height.toFloat() + + val scaleDetector = ScaleGestureDetector( + this, + object : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScale(d: ScaleGestureDetector): Boolean { + if (isGbaLocked) return true + val sf = d.scaleFactor + val ow = cw + val oh = ch + + // Scale width and maintain 3:2 aspect ratio + cw = (cw * sf).coerceIn(120f, 960f) + ch = cw * (2f / 3f) + + // Center the scaling transformation + view.x += (ow - cw) / 2f + view.y += (oh - ch) / 2f + + val p = view.layoutParams as FrameLayout.LayoutParams + p.width = cw.toInt() + p.height = ch.toInt() + view.layoutParams = p + + slotPrefs.edit() + .putFloat("gba_width", cw) + .putFloat("gba_height", ch) + .putFloat("gba_x", view.x) + .putFloat("gba_y", view.y) + .apply() + return true + } + }) + + view.setOnTouchListener { v, event -> + // Prevent interaction if Snap Mode (Locked) is active + if (isGbaLocked) return@setOnTouchListener false + + // Let the scale detector handle pinch gestures first + scaleDetector.onTouchEvent(event) + + // If the user is currently pinching/scaling, stop the dragging logic + if (scaleDetector.isInProgress) return@setOnTouchListener true + + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + // Store the offset so the window doesn't "jump" to the finger center + dragX = event.rawX - v.x + dragY = event.rawY - v.y + } + + MotionEvent.ACTION_MOVE -> { + // Update the view's position as the finger moves + v.x = event.rawX - dragX + v.y = event.rawY - dragY + } + + MotionEvent.ACTION_UP -> { + // Double tap detection for visibility toggle + val now = System.currentTimeMillis() + val last = lastGbaTapTimes[slot] ?: 0L + if (now - last < 300) { + view.onDoubleTap() + } + lastGbaTapTimes[slot] = now + + slotPrefs.edit() + .putFloat("gba_x", v.x) + .putFloat("gba_y", v.y) + .apply() + } + } + true + } + } + + //Android Gba layout + private fun applyGbaLayout() { + if (gbaViews.isEmpty()) + return + val tw = binding.root.width + val th = binding.root.height + val count = gbaViews.size + val isLandscape = + resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE + + binding.frameEmulationFragment.x = 0f + binding.frameEmulationFragment.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + binding.frameEmulationFragment.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + binding.frameEmulationFragment.requestLayout() + + if (isGbaLocked) { + if (isLandscape) { + val slotH = th / count + val maxW = (tw * 0.45f).toInt() + // Set point for landscape height + val maxH = 300 + + gbaViews.forEachIndexed { i, v -> + v.setOnTouchListener(null) + val p = v.layoutParams as FrameLayout.LayoutParams + + // Calculate height first, then width based on 3:2 ratio + var targetH = slotH.coerceAtMost(maxH) + var targetW = (targetH * 3f / 2f).toInt() + + // If it's too wide for the sidebar, scale down based on width + if (targetW > maxW) { + targetW = maxW + targetH = (targetW * 2f / 3f).toInt() + } + + p.width = targetW + p.height = targetH + v.layoutParams = p + + // Re-snapping: Force X to 0 and calculate centered Y within the slot + v.x = 0f + v.y = (i * slotH).toFloat() + (slotH - targetH) / 2f + v.visibility = android.view.View.VISIBLE + } + } else { + // Portrait Logic + val gih = (tw * 3f / 4f).toInt() + val topBar = (th - gih) / 2 + val gbaY = topBar + gih + val availH = th - gbaY + // Set point for portrait height + val maxH = 400 + + when (count) { + 1 -> { + val targetH = (tw * 2f / 3f).toInt().coerceAtMost(availH).coerceAtMost(maxH) + val targetW = (targetH * 3f / 2f).toInt() + + with(gbaViews[0]) { + setOnTouchListener(null) + val p = layoutParams as FrameLayout.LayoutParams + p.width = targetW; p.height = targetH; layoutParams = p + x = (tw - targetW) / 2f + y = gbaY.toFloat() + (availH - targetH) / 2f + visibility = android.view.View.VISIBLE + } + } + + else -> { + // Multi-screen grid (2, 3, or 4) + val cols = if (count <= 2) count else 2 + val rows = if (count <= 2) 1 else 2 + val slotW = tw / cols + val slotH = availH / rows + + val targetH = + (slotW * 2f / 3f).toInt().coerceAtMost(slotH).coerceAtMost(maxH) + val targetW = (targetH * 3f / 2f).toInt() + + gbaViews.forEachIndexed { i, v -> + v.setOnTouchListener(null) + val p = v.layoutParams as FrameLayout.LayoutParams + p.width = targetW; p.height = targetH; v.layoutParams = p + + val col = i % cols + val row = i / cols + + v.x = (col * slotW).toFloat() + (slotW - targetW) / 2f + v.y = gbaY.toFloat() + (row * slotH).toFloat() + (slotH - targetH) / 2f + v.visibility = android.view.View.VISIBLE + } + } + } + } + } else { + gbaViews.forEachIndexed { i, view -> + val slot = view.gbaSlot + val sp2 = getSharedPreferences("gba_overlay_${slot}", Context.MODE_PRIVATE) + val sw = sp2.getFloat("gba_width", 480f).coerceIn(120f, 960f) + val sh = sp2.getFloat("gba_height", 320f).coerceIn(80f, 640f) + val screenW = resources.displayMetrics.widthPixels.toFloat() + val screenH = resources.displayMetrics.heightPixels.toFloat() + var sx = sp2.getFloat("gba_x", 16f + i * 20f) + var sy = sp2.getFloat("gba_y", screenH - sh - 16f - i * 20f) + if (sx < 0 || sx > screenW) sx = 16f + i * 20f + if (sy < 0 || sy > screenH) sy = screenH - sh - 16f + val p = view.layoutParams as FrameLayout.LayoutParams + p.width = sw.toInt(); p.height = sh.toInt(); view.layoutParams = p + view.x = sx; view.y = sy + attachGbaTouchListener(view, slot, sp2) + } + } + getSharedPreferences("gba_overlay", Context.MODE_PRIVATE).edit() + .putBoolean("gba_locked", isGbaLocked).apply() + } + + private fun toggleGBASnap() { + isGbaLocked = !isGbaLocked + if (!isGbaLocked) { + NativeLibrary.SetObscuredPixelsLeft(0) + } + binding.root.post { applyGbaLayout() } + + } + + private fun resetGBAScreens() { + if (gbaViews.isEmpty()) return + runOnUiThread { + val screenH = resources.displayMetrics.heightPixels.toFloat() + gbaViews.forEachIndexed { i, view -> + val slot = view.gbaSlot + val dx = 16f + i * 20f + val dy = screenH - 320f - 16f - i * 20f + getSharedPreferences("gba_overlay_${slot}", Context.MODE_PRIVATE).edit() + .putFloat("gba_x", dx).putFloat("gba_y", dy).putFloat("gba_width", 480f) + .putFloat("gba_height", 320f).apply() + val p = view.layoutParams as? FrameLayout.LayoutParams ?: return@forEachIndexed + p.width = 480; p.height = 320; view.layoutParams = p + view.x = dx; view.y = dy + } + } + } + + private fun resetGbaCore() { + for (slot in 0 until 4) { + if (IntSetting.getSettingForSIDevice(slot).int == InputOverlay.EMULATED_GBA_CONTROLLER) + GbaLibrary.resetGbaCore(slot) + } + } + + companion object { private const val BACKSTACK_NAME_MENU = "menu" private const val BACKSTACK_NAME_SUBMENU = "submenu" @@ -1077,6 +1475,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { const val MENU_ACTION_SKYLANDERS = 36 const val MENU_ACTION_INFINITY_BASE = 37 const val MENU_ACTION_LATCHING_CONTROLS = 38 + const val MENU_ACTION_GBA_SNAP = 39 + const val MENU_ACTION_GBA_RESET = 40 + const val MENU_ACTION_GBA_RESET_CORE = 41 init { buttonsActionsMap.apply { @@ -1090,19 +1491,38 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { append(R.id.menu_emulation_ir_recenter, MENU_SET_IR_RECENTER) append(R.id.menu_emulation_set_ir_mode, MENU_SET_IR_MODE) append(R.id.menu_emulation_choose_doubletap, MENU_ACTION_CHOOSE_DOUBLETAP) + append(R.id.menu_emulation_gba_snap, MENU_ACTION_GBA_SNAP) + append(R.id.menu_emulation_gba_reset, MENU_ACTION_GBA_RESET) + append(R.id.menu_emulation_gba_reset_core, MENU_ACTION_GBA_RESET_CORE) } } @JvmStatic - fun launch(activity: FragmentActivity, filePaths: Array, riivolution: Boolean, fromIntent: Boolean = false) { + fun launch( + activity: FragmentActivity, + filePaths: Array, + riivolution: Boolean, + fromIntent: Boolean = false + ) { if (ignoreLaunchRequests) return - performLaunchChecks(activity, fromIntent) { launchWithoutChecks(activity, filePaths, riivolution) } + performLaunchChecks(activity, fromIntent) { + launchWithoutChecks( + activity, + filePaths, + riivolution + ) + } } @JvmStatic - fun launch(activity: FragmentActivity, filePath: String, riivolution: Boolean, fromIntent: Boolean = false) = + fun launch( + activity: FragmentActivity, + filePath: String, + riivolution: Boolean, + fromIntent: Boolean = false + ) = launch(activity, arrayOf(filePath), riivolution, fromIntent) private fun launchWithoutChecks( @@ -1117,7 +1537,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { activity.startActivity(launcher) } - private fun performLaunchChecks(activity: FragmentActivity, fromIntent: Boolean, continueCallback: Runnable) { + private fun performLaunchChecks( + activity: FragmentActivity, + fromIntent: Boolean, + continueCallback: Runnable + ) { AfterDirectoryInitializationRunner().runWithLifecycle(activity) { if (fromIntent) { activity.finish() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaLibrary.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaLibrary.kt new file mode 100644 index 0000000000..fbf4a894d7 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaLibrary.kt @@ -0,0 +1,19 @@ +package org.dolphinemu.dolphinemu.features.gba + +import androidx.annotation.Keep +import org.dolphinemu.dolphinemu.features.gba.GbaRenderManager + +object GbaLibrary{ + + @JvmStatic + external fun copyGbaFramebuffer(slot: Int, buffer: java.nio.ByteBuffer): Boolean + + @JvmStatic + external fun resetGbaCore(slot: Int) + + @Keep + @JvmStatic + fun onGbaFrame(slot: Int) { + GbaRenderManager.onFrame(slot) + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaOverlayView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaOverlayView.kt new file mode 100644 index 0000000000..aa57203ab3 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaOverlayView.kt @@ -0,0 +1,80 @@ +package org.dolphinemu.dolphinemu.features.gba + +import android.content.Context +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.graphics.* + +//Passive surfaceview displaying one gba screen +//Rendering is driven by GbaRenderManager + +class GbaOverlayView(context: Context) : SurfaceView(context), + SurfaceHolder.Callback { + var renderManager: GbaRenderManager? = null + var gbaSlot: Int = 0 + var isScreenVisible = true + var needsBorderRedraw = false + var surfaceReady = false + + private val paint = Paint().apply { isFilterBitmap = true } + private val destRect = Rect() + private val borderPaint = Paint().apply { + color = Color.argb(120, 255, 255, 255); style = Paint.Style.STROKE + strokeWidth = 2f; isAntiAlias = true + } + private val borderFillPaint = Paint().apply { + color = Color.argb(0, 0, 0, 0); style = Paint.Style.FILL + } + private val borderTextPaint = Paint().apply { + color = Color.argb(120, 255, 255, 255); textSize = 20f + isAntiAlias = true; typeface = Typeface.DEFAULT_BOLD + textAlign = Paint.Align.CENTER + } + + init { + holder.setFormat(PixelFormat.TRANSLUCENT) + setZOrderMediaOverlay(true) + holder.addCallback(this) + } + + fun drawFrame(bitmap: Bitmap) { + if (!holder.surface.isValid) return + if (!isScreenVisible) { + if (needsBorderRedraw) { + needsBorderRedraw = false + val canvas = holder.lockCanvas() ?: return + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + val rect = RectF(2f, 2f, width - 2f, height - 2f) + canvas.drawRoundRect(rect, 12f, 12f, borderFillPaint) + canvas.drawRoundRect(rect, 12f, 12f, borderPaint) + canvas.drawText("GBA", width / 2f, height / 2f + 8f, borderTextPaint) + holder.unlockCanvasAndPost(canvas) + } + return + } + val canvas = holder.lockCanvas() ?: return + destRect.set(0, 0, width, height) + canvas.drawColor(Color.BLACK) + canvas.drawBitmap(bitmap, null, destRect, paint) + holder.unlockCanvasAndPost(canvas) + } + + fun onDoubleTap() { + isScreenVisible = !isScreenVisible + if (!isScreenVisible) needsBorderRedraw = true + } + + override fun surfaceCreated(h: SurfaceHolder) { + surfaceReady = false + } + + override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, h2: Int) { + surfaceReady = true + post { renderManager?.requestRedraw(gbaSlot) } + } + + override fun surfaceDestroyed(h: SurfaceHolder) { + surfaceReady = false + } +} + diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaRenderManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaRenderManager.kt new file mode 100644 index 0000000000..3198bd56b2 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaRenderManager.kt @@ -0,0 +1,70 @@ +package org.dolphinemu.dolphinemu.features.gba + +import android.graphics.Bitmap +import android.os.Handler +import android.os.HandlerThread +import org.dolphinemu.dolphinemu.NativeLibrary +import java.nio.ByteBuffer + +object GbaRenderManager { + + private val buffers = Array(4) { ByteBuffer.allocateDirect(240 * 160 * 4) } + private val bitmaps = Array(4) { Bitmap.createBitmap(240, 160, Bitmap.Config.ARGB_8888) } + private val renderThread = HandlerThread("GBA_RENDER").apply { start() } + private val handler = Handler(renderThread.looper) + + @Volatile + private var activeViews: List = emptyList() + + @Volatile + private var attached = false + + fun isAttached() = attached + + fun onFrame(slot: Int) { + if (!attached || slot < 0 || slot >= 4) return + handler.post { renderFrame(slot, forceRedraw = false) } + } + + private fun renderFrame(slot: Int, forceRedraw: Boolean = false) { + val view = activeViews.firstOrNull { it.gbaSlot == slot } ?: return + if (!view.holder.surface.isValid) return + val buffer = buffers[slot] + buffer.rewind() + if (GbaLibrary.copyGbaFramebuffer(slot, buffer)) { + buffer.rewind() + bitmaps[slot].copyPixelsFromBuffer(buffer) + view.drawFrame(bitmaps[slot]) + } else if (forceRedraw) { + view.drawFrame(bitmaps[slot]) + } + } + + fun requestRedraw(slot: Int) { + if (!attached || slot < 0 || slot >= 4) return + handler.post { renderFrame(slot, forceRedraw = true) } + } + + fun attach(views: List) { + handler.removeCallbacksAndMessages(null) + attached = true + activeViews = views + views.forEach { + it.renderManager = this + if (it.surfaceReady) requestRedraw(it.gbaSlot) + } + } + + fun updateViews(views: List) { + activeViews = views + views.forEach { it.renderManager = this } + views.forEach { if (it.surfaceReady) requestRedraw(it.gbaSlot) } + } + + fun detach() { + handler.removeCallbacksAndMessages(null) + //renderThread.quitSafely() + activeViews = emptyList() + attached = false + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.kt index 8224434ee6..51137f0b71 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/InputOverrider.kt @@ -11,6 +11,10 @@ object InputOverrider { external fun unregisterWii(controllerIndex: Int) + external fun registerGBA(controllerIndex: Int) + + external fun unregisterGBA(controllerIndex: Int) + external fun setControlState(controllerIndex: Int, control: Int, state: Double) external fun clearControlState(controllerIndex: Int, control: Int) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt index 512a33ddbb..3b1f292df0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt @@ -72,6 +72,10 @@ enum class StringSetting( MAIN_GBA_BIOS_PATH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "BIOS", ""), MAIN_GB_PLAYER_ROM(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "GBPlayerRom", ""), MAIN_GBA_SAVES_PATH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "SavesPath", ""), + MAIN_GBA_ROM_PATH_1(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "Rom1", ""), + MAIN_GBA_ROM_PATH_2(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "Rom2", ""), + MAIN_GBA_ROM_PATH_3(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "Rom3", ""), + MAIN_GBA_ROM_PATH_4(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GBA, "Rom4", ""), MAIN_TRIFORCE_IP_REDIRECTIONS( Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, @@ -137,6 +141,14 @@ enum class StringSetting( MAIN_GFX_BACKEND ) + fun getGBARomPath(slot: Int): StringSetting = when (slot) { + 0 -> MAIN_GBA_ROM_PATH_1 + 1 -> MAIN_GBA_ROM_PATH_2 + 2 -> MAIN_GBA_ROM_PATH_3 + 3 -> MAIN_GBA_ROM_PATH_4 + else -> MAIN_GBA_ROM_PATH_1 + } + private val NOT_RUNTIME_EDITABLE: Set = HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY)) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt index ae236d3764..6db93cad6f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -251,6 +251,7 @@ class SettingsAdapter( slider.valueTo = item.max slider.stepSize = item.stepSize } + is IntSliderSetting -> { slider.valueFrom = item.min.toFloat() slider.valueTo = item.max.toFloat() @@ -476,6 +477,7 @@ class SettingsAdapter( closeDialog() } + is SingleChoiceSettingDynamicDescriptions -> { val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions @@ -486,6 +488,7 @@ class SettingsAdapter( closeDialog() } + is StringSingleChoiceSetting -> { val scSetting = clickedItem as StringSingleChoiceSetting @@ -496,6 +499,7 @@ class SettingsAdapter( closeDialog() } + is IntSliderSetting -> { val sliderSetting = clickedItem as IntSliderSetting if (sliderSetting.selectedValue != seekbarProgress.toInt()) { @@ -504,6 +508,7 @@ class SettingsAdapter( sliderSetting.setSelectedValue(settings!!, seekbarProgress.toInt()) closeDialog() } + is FloatSliderSetting -> { val sliderSetting = clickedItem as FloatSliderSetting diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt index 32a7a64025..3efc54db96 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt @@ -502,16 +502,16 @@ class SettingsFragmentPresenter( override val isOverridden: Boolean get() = BooleanSetting.MAIN_DSP_HLE.isOverridden || - BooleanSetting.MAIN_DSP_JIT.isOverridden + BooleanSetting.MAIN_DSP_JIT.isOverridden override val isRuntimeEditable: Boolean get() = BooleanSetting.MAIN_DSP_HLE.isRuntimeEditable && - BooleanSetting.MAIN_DSP_JIT.isRuntimeEditable + BooleanSetting.MAIN_DSP_JIT.isRuntimeEditable override fun delete(settings: Settings): Boolean { // Not short circuiting return BooleanSetting.MAIN_DSP_HLE.delete(settings) and - BooleanSetting.MAIN_DSP_JIT.delete(settings) + BooleanSetting.MAIN_DSP_JIT.delete(settings) } } @@ -1002,8 +1002,8 @@ class SettingsFragmentPresenter( 0, false ) { - fragmentView.showDialogFragment(LoginDialog(this)) - loadSettingsList() + fragmentView.showDialogFragment(LoginDialog(this)) + loadSettingsList() }) } else { sl.add( @@ -1015,8 +1015,8 @@ class SettingsFragmentPresenter( 0, false ) { - logout() - loadSettingsList() + logout() + loadSettingsList() }) } sl.add( @@ -1113,16 +1113,16 @@ class SettingsFragmentPresenter( override val isOverridden: Boolean get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isOverridden || - BooleanSetting.MAIN_SYNC_GPU.isOverridden + BooleanSetting.MAIN_SYNC_GPU.isOverridden override val isRuntimeEditable: Boolean get() = BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.isRuntimeEditable && - BooleanSetting.MAIN_SYNC_GPU.isRuntimeEditable + BooleanSetting.MAIN_SYNC_GPU.isRuntimeEditable override fun delete(settings: Settings): Boolean { // Not short circuiting return BooleanSetting.MAIN_SYNC_ON_SKIP_IDLE.delete(settings) and - BooleanSetting.MAIN_SYNC_GPU.delete(settings) + BooleanSetting.MAIN_SYNC_GPU.delete(settings) } } @@ -2247,7 +2247,7 @@ class SettingsFragmentPresenter( BooleanSetting.MAIN_DEBUG_JIT_ENABLE_PROFILING, R.string.debug_jit_enable_block_profiling, 0 - ) + ) ) sl.add( RunRunnable( @@ -2413,6 +2413,7 @@ class SettingsFragmentPresenter( addControllerMappingSettings(sl, gcPad, null) } } + 7 -> { // Emulated keyboard controller val gcKeyboard = EmulatedController.getGcKeyboard(gcPadNumber) @@ -2425,6 +2426,7 @@ class SettingsFragmentPresenter( addControllerMappingSettings(sl, gcKeyboard, null) } } + 12 -> { // Adapter sl.add( @@ -2444,6 +2446,21 @@ class SettingsFragmentPresenter( ) ) } + + 13 -> { + //GBA emulator + sl.add(HeaderSetting(context, R.string.gba_settings, 0)) + sl.add( + FilePicker( + context, + StringSetting.getGBARomPath(gcPadNumber), + R.string.gba_rom_path, + R.string.gba_rom_path_description, + fragmentView.activityResultLaunchers.requestGbaRomFile, + null + ) + ) + } } } @@ -2643,11 +2660,11 @@ class SettingsFragmentPresenter( * @param groupTypeFilter If this is non-null, only groups whose types match this are considered. */ private fun addControllerMappingSettings( - sl: ArrayList, - controller: EmulatedController, - groupTypeFilter: Set? + sl: ArrayList, + controller: EmulatedController, + groupTypeFilter: Set? ) { - addContainerMappingSettings(sl, controller, controller, groupTypeFilter) + addContainerMappingSettings(sl, controller, controller, groupTypeFilter) } /** @@ -2742,7 +2759,7 @@ class SettingsFragmentPresenter( val defaultDevice = controller.getDefaultDevice() hasOldControllerSettings = defaultDevice.startsWith("Android/") && - defaultDevice.endsWith("/Touchscreen") + defaultDevice.endsWith("/Touchscreen") fragmentView.setOldControllerSettingsWarningVisibility(hasOldControllerSettings) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt index 3d991fed1e..93b1c3cc0a 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt @@ -51,11 +51,17 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex private var isFirstRun = true private val gcPadRegistered = BooleanArray(4) private val wiimoteRegistered = BooleanArray(4) + private val gbaRegistered = BooleanArray(4) + private val gbaOverlayButtons: MutableSet = HashSet() + private val gbaOverlayDpads: MutableSet = HashSet() + private var gbaControllerIndex = -1 var editMode = false private var controllerType = -1 private var controllerIndex = 0 private var buttonBeingConfigured: InputOverlayDrawableButton? = null private var dpadBeingConfigured: InputOverlayDrawableDpad? = null + private var gbaButtonBeingConfigured: InputOverlayDrawableButton? = null + private var gbaDpadBeingConfigured: InputOverlayDrawableDpad? = null private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null private val preferences: SharedPreferences @@ -129,6 +135,35 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex for (joystick in overlayJoysticks) { joystick.draw(canvas) } + + for (button in gbaOverlayButtons) { + button.draw(canvas) + drawGBABadge(canvas, button.bounds) + } + + for (dpad in gbaOverlayDpads) { + dpad.draw(canvas) + drawGBABadge(canvas, dpad.bounds) + } + } + + //draws gba badge on controlls for gba controller, to not get confused with the GC pad buttons + private fun drawGBABadge(canvas: Canvas, bounds: android.graphics.Rect) { + val bp = android.graphics.Paint().apply { + isAntiAlias = true; color = android.graphics.Color.argb(200, 98, 0, 238) + style = android.graphics.Paint.Style.FILL + } + val tp = android.graphics.Paint().apply { + isAntiAlias = true; color = android.graphics.Color.WHITE; textSize = 18f + typeface = android.graphics.Typeface.DEFAULT_BOLD + textAlign = android.graphics.Paint.Align.CENTER + } + val r = android.graphics.RectF( + bounds.left.toFloat(), bounds.top.toFloat(), + bounds.left + 36f, bounds.top + 18f + ) + canvas.drawRoundRect(r, 6f, 6f, bp) + canvas.drawText("GBA", r.left + 18f, r.top + 14f, tp) } override fun onTouch(v: View, event: MotionEvent): Boolean { @@ -138,26 +173,107 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex val action = event.actionMasked val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN && - action != MotionEvent.ACTION_POINTER_UP + action != MotionEvent.ACTION_POINTER_UP val pointerIndex = if (firstPointer) 0 else event.actionIndex // Tracks if any button/joystick is pressed down var pressed = false +// Process GBA buttons first and claim them so GCPAD doesnt fire on the same touch. + val gbaClaimedPointers = mutableSetOf() - for (button in overlayButtons) { + if (gbaControllerIndex >= 0) { + pressed = processButtons( + gbaOverlayButtons, + gbaControllerIndex, + action, + event, + pointerIndex, + pressed, + gbaClaimedPointers + ) + pressed = processDpads( + gbaOverlayDpads, + gbaControllerIndex, + action, + event, + pointerIndex, + pressed, + gbaClaimedPointers + ) + } + + pressed = processButtons(overlayButtons, controllerIndex, action, event, pointerIndex, pressed, excludePointers = gbaClaimedPointers) + pressed = processDpads(overlayDpads, controllerIndex, action, event, pointerIndex, pressed, excludePointers = gbaClaimedPointers) + + for (joystick in overlayJoysticks) { + if (joystick.trackEvent(event)) { + if (joystick.trackId != -1) + pressed = true + } + + InputOverrider.setControlState( + controllerIndex, + joystick.xControl, + joystick.x.toDouble() + ) + InputOverrider.setControlState( + controllerIndex, + joystick.yControl, + -joystick.y.toDouble() + ) + } + + // No button/joystick pressed, safe to move pointer + if (!pressed && overlayPointer != null) { + overlayPointer!!.onTouch(event) + InputOverrider.setControlState( + controllerIndex, + ControlId.WIIMOTE_IR_X, + overlayPointer!!.x.toDouble() + ) + InputOverrider.setControlState( + controllerIndex, + ControlId.WIIMOTE_IR_Y, + -overlayPointer!!.y.toDouble() + ) + } + + invalidate() + + return true + } + + fun processButtons( + buttons: Set, + controllerIndex: Int, + action: Int, + event: MotionEvent, + pointerIndex: Int, + pressedIn: Boolean, + claimedPointers: MutableSet = mutableSetOf(), + excludePointers: Set = emptySet() + ): Boolean { + var pressed = pressedIn + for (button in buttons) { // Determine the button state to apply based on the MotionEvent action flag. when (action) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + val pointerId = event.getPointerId(pointerIndex) // If a pointer enters the bounds of a button, press that button. - if (button.bounds.contains( + if (!excludePointers.contains(pointerId) && button.bounds.contains( event.getX(pointerIndex).toInt(), event.getY(pointerIndex).toInt() ) ) { button.setPressedState(if (button.latching) !button.getPressedState() else true) - button.trackId = event.getPointerId(pointerIndex) + button.trackId = pointerId pressed = true - InputOverrider.setControlState(controllerIndex, button.control, if (button.getPressedState()) 1.0 else 0.0) + claimedPointers.add(pointerId) + InputOverrider.setControlState( + controllerIndex, + button.control, + if (button.getPressedState()) 1.0 else 0.0 + ) val analogControl = getAnalogControlForTrigger(button.control) if (analogControl >= 0) @@ -175,7 +291,11 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex if (button.trackId == event.getPointerId(pointerIndex)) { if (!button.latching) button.setPressedState(false) - InputOverrider.setControlState(controllerIndex, button.control, if (button.getPressedState()) 1.0 else 0.0) + InputOverrider.setControlState( + controllerIndex, + button.control, + if (button.getPressedState()) 1.0 else 0.0 + ) val analogControl = getAnalogControlForTrigger(button.control) if (analogControl >= 0) @@ -190,14 +310,29 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex } } } + return pressed + } - for (dpad in overlayDpads) { + fun processDpads( + dpads: Set, + controllerIndex: Int, + action: Int, + event: MotionEvent, + pointerIndex: Int, + pressedIn: Boolean, + claimedPointers: MutableSet = mutableSetOf(), + excludePointers: Set = emptySet() + ): Boolean + { + var pressed = pressedIn + for (dpad in dpads) { // Determine the button state to apply based on the MotionEvent action flag. when (event.action and MotionEvent.ACTION_MASK) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + val pointerId = event.getPointerId(pointerIndex) // If a pointer enters the bounds of a button, press that button. - if (dpad.bounds + if (!excludePointers.contains(pointerId) && dpad.bounds .contains( event.getX(pointerIndex).toInt(), event.getY(pointerIndex).toInt() @@ -205,6 +340,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex ) { dpad.trackId = event.getPointerId(pointerIndex) pressed = true + claimedPointers.add(pointerId) } } } @@ -267,43 +403,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex } } } - - for (joystick in overlayJoysticks) { - if (joystick.trackEvent(event)) { - if (joystick.trackId != -1) - pressed = true - } - - InputOverrider.setControlState( - controllerIndex, - joystick.xControl, - joystick.x.toDouble() - ) - InputOverrider.setControlState( - controllerIndex, - joystick.yControl, - -joystick.y.toDouble() - ) - } - - // No button/joystick pressed, safe to move pointer - if (!pressed && overlayPointer != null) { - overlayPointer!!.onTouch(event) - InputOverrider.setControlState( - controllerIndex, - ControlId.WIIMOTE_IR_X, - overlayPointer!!.x.toDouble() - ) - InputOverrider.setControlState( - controllerIndex, - ControlId.WIIMOTE_IR_Y, - -overlayPointer!!.y.toDouble() - ) - } - - invalidate() - - return true + return pressed } fun onTouchWhileEditing(event: MotionEvent): Boolean { @@ -425,6 +525,76 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex } } } + for (button in gbaOverlayButtons) { + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + if (gbaButtonBeingConfigured == null && + button.bounds.contains(fingerPositionX, fingerPositionY) + ) { + gbaButtonBeingConfigured = button + gbaButtonBeingConfigured?.onConfigureTouch(event) + } + } + + MotionEvent.ACTION_MOVE -> { + if (gbaButtonBeingConfigured != null) { + gbaButtonBeingConfigured?.onConfigureTouch(event) + invalidate() + return true + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + if (gbaButtonBeingConfigured == button) { + saveControlPosition( + gbaButtonBeingConfigured!!.legacyId, + gbaButtonBeingConfigured!!.bounds.left, + gbaButtonBeingConfigured!!.bounds.top, + orientation + ) + gbaButtonBeingConfigured = null + } + } + } + } + for (dpad in gbaOverlayDpads) { + when (event.action and MotionEvent.ACTION_MASK) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_POINTER_DOWN -> { + if (gbaDpadBeingConfigured == null && dpad.bounds.contains( + fingerPositionX, + fingerPositionY + ) + ) { + gbaDpadBeingConfigured = dpad + gbaDpadBeingConfigured?.onConfigureTouch(event) + } + } + + MotionEvent.ACTION_MOVE -> { + if (gbaDpadBeingConfigured != null) { + gbaDpadBeingConfigured?.onConfigureTouch(event) + invalidate() + return true + } + } + + MotionEvent.ACTION_UP, + MotionEvent.ACTION_POINTER_UP -> { + if (gbaDpadBeingConfigured == dpad) { + saveControlPosition( + gbaDpadBeingConfigured!!.legacyId, + gbaDpadBeingConfigured!!.bounds.left, + gbaDpadBeingConfigured!!.bounds.top, + orientation + ) + gbaDpadBeingConfigured = null + } + } + } + } return true } @@ -443,8 +613,16 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex InputOverrider.unregisterWii(i) } + for (i in gbaRegistered.indices) { + if (gbaRegistered[i]) InputOverrider.unregisterGBA(i) + } + Arrays.fill(gcPadRegistered, false) Arrays.fill(wiimoteRegistered, false) + Arrays.fill(gbaRegistered, false) + gbaOverlayButtons.clear() + gbaOverlayDpads.clear() + gbaControllerIndex = -1 } private fun getAnalogControlForTrigger(control: Int): Int = when (control) { @@ -640,6 +818,70 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex } } + private fun addGBAOverlayControls(orientation: String) { + gbaOverlayButtons.add( + initializeOverlayButton( + context, R.drawable.gcpad_a, R.drawable.gcpad_a_pressed, + ButtonType.BUTTON_A + GBA_BUTTON_ID_OFFSET, + ControlId.GCPAD_A_BUTTON, orientation, false + ) + ) + + gbaOverlayButtons.add( + initializeOverlayButton( + context, R.drawable.gcpad_b, R.drawable.gcpad_b_pressed, + ButtonType.BUTTON_B + GBA_BUTTON_ID_OFFSET, + ControlId.GCPAD_B_BUTTON, orientation, false + ) + ) + + gbaOverlayButtons.add( + initializeOverlayButton( + context, + R.drawable.gcpad_start, + R.drawable.gcpad_start_pressed, + ButtonType.BUTTON_START + GBA_BUTTON_ID_OFFSET, + ControlId.GCPAD_START_BUTTON, + orientation, + false + ) + ) + + gbaOverlayButtons.add( + initializeOverlayButton( + context, R.drawable.gcpad_z, R.drawable.gcpad_z_pressed, + ButtonType.BUTTON_Z + GBA_BUTTON_ID_OFFSET, + ControlId.GCPAD_Z_BUTTON, orientation, false + ) + ) + + gbaOverlayButtons.add( + initializeOverlayButton( + context, R.drawable.gcpad_l, R.drawable.gcpad_l_pressed, + ButtonType.TRIGGER_L + GBA_BUTTON_ID_OFFSET, + ControlId.GCPAD_L_DIGITAL, orientation, false + ) + ) + + gbaOverlayButtons.add( + initializeOverlayButton( + context, R.drawable.gcpad_r, R.drawable.gcpad_r_pressed, + ButtonType.TRIGGER_R + GBA_BUTTON_ID_OFFSET, + ControlId.GCPAD_R_DIGITAL, orientation, false + ) + ) + + gbaOverlayDpads.add( + initializeOverlayDpad( + context, R.drawable.gcwii_dpad, R.drawable.gcwii_dpad_pressed_one_direction, + R.drawable.gcwii_dpad_pressed_two_directions, + ButtonType.BUTTON_UP + GBA_BUTTON_ID_OFFSET, + ControlId.GCPAD_DPAD_UP, ControlId.GCPAD_DPAD_DOWN, + ControlId.GCPAD_DPAD_LEFT, ControlId.GCPAD_DPAD_RIGHT, orientation + ) + ) + } + private fun addWiimoteOverlayControls(orientation: String) { if (BooleanSetting.MAIN_BUTTON_TOGGLE_WII_0.boolean) { overlayButtons.add( @@ -1047,6 +1289,23 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex OVERLAY_NONE -> {} } + // add GBA controls on top of primary, GC controller always visible + gbaOverlayButtons.clear() + gbaOverlayDpads.clear() + gbaControllerIndex = -1 + + for (i in 0 until 4) { + if (getSettingForSIDevice(i).int == EMULATED_GBA_CONTROLLER) { + if (gbaControllerIndex < 0) gbaControllerIndex = i + if (!gbaRegistered[i]) { + InputOverrider.registerGBA(i) + gbaRegistered[i] = true + } + } + } + if (gbaControllerIndex >= 0) { + addGBAOverlayControls(orientation) + } } isFirstRun = false @@ -1085,6 +1344,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex wiiOnlyPortraitDefaultOverlay() } } + if (isLandscape) gbaDefaultOverlay() else gbaPortraitDefaultOverlay() refreshControls() } @@ -1401,6 +1661,22 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex ) { wiiClassicPortraitDefaultOverlay() } + + // GBA controls android + if (preferences.getFloat( + (ButtonType.BUTTON_A + GBA_BUTTON_ID_OFFSET) + .toString() + "-X", 0f + ) == 0f + ) { + gbaDefaultOverlay() + } + if (preferences.getFloat( + (ButtonType.BUTTON_A + GBA_BUTTON_ID_OFFSET) + .toString() + "-Portrait" + "-X", 0f + ) == 0f + ) { + gbaPortraitDefaultOverlay() + } } if (!preferences.getBoolean("OverlayInitV3", false)) { @@ -2276,6 +2552,120 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex .apply() } + private fun gbaDefaultOverlay() { + val dm = resources.displayMetrics + var maxX = dm.heightPixels.toFloat() + var maxY = dm.widthPixels.toFloat() + if (maxY > maxX) { + val tmp = maxX; + maxX = maxY; + maxY = tmp + } + + preferences.edit() + .putFloat((ButtonType.BUTTON_A + GBA_BUTTON_ID_OFFSET).toString() + "-X", 0.82f * maxX) + .putFloat((ButtonType.BUTTON_A + GBA_BUTTON_ID_OFFSET).toString() + "-Y", 0.60f * maxY) + .putFloat((ButtonType.BUTTON_B + GBA_BUTTON_ID_OFFSET).toString() + "-X", 0.73f * maxX) + .putFloat((ButtonType.BUTTON_B + GBA_BUTTON_ID_OFFSET).toString() + "-Y", 0.70f * maxY) + .putFloat((ButtonType.TRIGGER_L + GBA_BUTTON_ID_OFFSET).toString() + "-X", 0.08f * maxX) + .putFloat((ButtonType.TRIGGER_L + GBA_BUTTON_ID_OFFSET).toString() + "-Y", 0.25f * maxY) + .putFloat((ButtonType.TRIGGER_R + GBA_BUTTON_ID_OFFSET).toString() + "-X", 0.78f * maxX) + .putFloat((ButtonType.TRIGGER_R + GBA_BUTTON_ID_OFFSET).toString() + "-Y", 0.25f * maxY) + .putFloat( + (ButtonType.BUTTON_START + GBA_BUTTON_ID_OFFSET).toString() + "-X", + 0.60f * maxX + ) + .putFloat( + (ButtonType.BUTTON_START + GBA_BUTTON_ID_OFFSET).toString() + "-Y", + 0.80f * maxY + ) + .putFloat((ButtonType.BUTTON_Z + GBA_BUTTON_ID_OFFSET).toString() + "-X", 0.45f * maxX) + .putFloat((ButtonType.BUTTON_Z + GBA_BUTTON_ID_OFFSET).toString() + "-Y", 0.80f * maxY) + .putFloat((ButtonType.BUTTON_UP + GBA_BUTTON_ID_OFFSET).toString() + "-X", 0.12f * maxX) + .putFloat((ButtonType.BUTTON_UP + GBA_BUTTON_ID_OFFSET).toString() + "-Y", 0.55f * maxY) + .apply() + } + + private fun gbaPortraitDefaultOverlay() { + val dm = resources.displayMetrics + var maxX = dm.heightPixels.toFloat() + var maxY = dm.widthPixels.toFloat() + if (maxY < maxX) { + val tmp = maxX; + maxX = maxY; + maxY = tmp + } + val portrait = "-Portrait" + + preferences.edit() + .putFloat( + (ButtonType.BUTTON_A + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-X", + 0.82f * maxX + ) + .putFloat( + (ButtonType.BUTTON_A + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-Y", + 0.72f * maxY + ) + .putFloat( + (ButtonType.BUTTON_B + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-X", + 0.68f * maxX + ) + .putFloat( + (ButtonType.BUTTON_B + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-Y", + 0.80f * maxY + ) + .putFloat( + (ButtonType.TRIGGER_L + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-X", + 0.04f * maxX + ) + .putFloat( + (ButtonType.TRIGGER_L + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-Y", + 0.55f * maxY + ) + .putFloat( + (ButtonType.TRIGGER_R + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-X", + 0.78f * maxX + ) + .putFloat( + (ButtonType.TRIGGER_R + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-Y", + 0.55f * maxY + ) + .putFloat( + (ButtonType.BUTTON_START + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-X", + 0.62f * maxX + ) + .putFloat( + (ButtonType.BUTTON_START + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-Y", + 0.90f * maxY + ) + .putFloat( + (ButtonType.BUTTON_Z + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-X", + 0.42f * maxX + ) + .putFloat( + (ButtonType.BUTTON_Z + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-Y", + 0.90f * maxY + ) + .putFloat( + (ButtonType.BUTTON_UP + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-X", + 0.10f * maxX + ) + .putFloat( + (ButtonType.BUTTON_UP + GBA_BUTTON_ID_OFFSET).toString() + portrait + "-Y", + 0.72f * maxY + ) + .apply() + } + + fun isTouchTracked(): Boolean { + return overlayButtons.any {it.trackId !=-1} || + return overlayDpads.any {it.trackId !=-1} || + return overlayJoysticks.any {it.trackId !=-1} || + return gbaOverlayButtons.any {it.trackId !=-1} || + return gbaOverlayDpads.any {it.trackId !=-1} + } + + companion object { const val OVERLAY_GAMECUBE = 0 const val OVERLAY_WIIMOTE = 1 @@ -2283,11 +2673,15 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex const val OVERLAY_WIIMOTE_NUNCHUK = 3 const val OVERLAY_WIIMOTE_CLASSIC = 4 const val OVERLAY_NONE = 5 + const val EMULATED_GBA_CONTROLLER = 13 private const val DISABLED_GAMECUBE_CONTROLLER = 0 private const val EMULATED_GAMECUBE_CONTROLLER = 6 private const val EMULATED_AM_BASEBOARD = 11 private const val GAMECUBE_ADAPTER = 12 + //avoid ID collision with GC buttons + private const val GBA_BUTTON_ID_OFFSET = 1000 + // Buttons that have special positions in Wiimote only private val WIIMOTE_H_BUTTONS = ArrayList() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java index f52a70216f..f1e4ccffeb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java @@ -29,8 +29,8 @@ import java.util.Set; public final class FileBrowserHelper { public static final HashSet GAME_EXTENSIONS = new HashSet<>(Arrays.asList( - "gcm", "tgc", "bin", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "nfs", "wad", "dol", - "elf", "json")); + "gcm", "tgc", "bin", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "nfs", "wad", "dol", + "elf", "json")); public static final HashSet GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS); @@ -40,19 +40,19 @@ public final class FileBrowserHelper } public static final HashSet GBA_ROM_EXTENSIONS = new HashSet<>(Arrays.asList( - "gba", "gbc", "gb", "agb", "mb", "rom", "bin")); + "gba", "gbc", "gb", "agb", "mb", "rom", "bin")); public static final HashSet BIN_EXTENSION = new HashSet<>(Collections.singletonList( - "bin")); + "bin")); public static final HashSet RAW_EXTENSION = new HashSet<>(Collections.singletonList( - "raw")); + "raw")); public static final HashSet WAD_EXTENSION = new HashSet<>(Collections.singletonList( - "wad")); + "wad")); public static Intent createDirectoryPickerIntent(FragmentActivity activity, - HashSet extensions) + HashSet extensions) { Intent i = new Intent(activity, CustomFilePickerActivity.class); @@ -60,7 +60,7 @@ public final class FileBrowserHelper i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); i.putExtra(FilePickerActivity.EXTRA_START_PATH, - Environment.getExternalStorageDirectory().getPath()); + Environment.getExternalStorageDirectory().getPath()); i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, extensions); return i; @@ -98,7 +98,7 @@ public final class FileBrowserHelper } public static void runAfterExtensionCheck(Context context, Uri uri, Set validExtensions, - Runnable runnable) + Runnable runnable) { String extension = null; @@ -123,18 +123,18 @@ public final class FileBrowserHelper else { int messageId = validExtensions.size() == 1 ? - R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple; + R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple; message = context.getString(messageId, extension, - setToSortedDelimitedString(validExtensions)); + setToSortedDelimitedString(validExtensions)); } new MaterialAlertDialogBuilder(context) - .setMessage(message) - .setPositiveButton(R.string.yes, (dialogInterface, i) -> runnable.run()) - .setNegativeButton(R.string.no, null) - .setCancelable(false) - .show(); + .setMessage(message) + .setPositiveButton(R.string.yes, (dialogInterface, i) -> runnable.run()) + .setNegativeButton(R.string.no, null) + .setCancelable(false) + .show(); } @Nullable diff --git a/Source/Android/app/src/main/res/layout/fragment_emulation.xml b/Source/Android/app/src/main/res/layout/fragment_emulation.xml index f51f25699a..c0f6f63c81 100644 --- a/Source/Android/app/src/main/res/layout/fragment_emulation.xml +++ b/Source/Android/app/src/main/res/layout/fragment_emulation.xml @@ -11,7 +11,9 @@ diff --git a/Source/Android/app/src/main/res/menu/menu_overlay_controls_gc.xml b/Source/Android/app/src/main/res/menu/menu_overlay_controls_gc.xml index e4ef459487..c21b8c0a03 100644 --- a/Source/Android/app/src/main/res/menu/menu_overlay_controls_gc.xml +++ b/Source/Android/app/src/main/res/menu/menu_overlay_controls_gc.xml @@ -30,4 +30,14 @@ + + + + + + diff --git a/Source/Android/app/src/main/res/values/arrays.xml b/Source/Android/app/src/main/res/values/arrays.xml index d919edb1d2..9cf4e4dedb 100644 --- a/Source/Android/app/src/main/res/values/arrays.xml +++ b/Source/Android/app/src/main/res/values/arrays.xml @@ -379,6 +379,7 @@ @string/gcpad_taru_konga @string/gcpad_am_baseboard @string/gcpad_gc_adapter + @string/gba_adapter 0 @@ -389,6 +390,7 @@ 10 11 12 + 13 diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 04c48bcba4..ca65c41f7e 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -89,6 +89,8 @@ BIOS Game Boy Player ROM Saves + GBA ROM Path + Rom to load for this slot. Wii Misc Settings SD Card Settings @@ -643,6 +645,7 @@ It can efficiently compress both junk data and encrypted Wii data. Enable the vibration function for this GameCube controller. Bongo Controller Enable this if you are using bongos on this port. + GBA (Integrated) Due to the Scoped Storage policy in Android 11 and newer, you can\'t change this path. Loading Settings… diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 18b486023a..ce8d5591fd 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -125,6 +125,9 @@ static jclass s_audio_utils_class; static jmethodID s_audio_utils_get_sample_rate; static jmethodID s_audio_utils_get_frames_per_buffer; +static jclass s_gba_library_class; +static jmethodID s_on_gba_frame; + namespace IDCache { JNIEnv* GetEnvForThread() @@ -575,11 +578,21 @@ jmethodID GetAudioUtilsGetFramesPerBuffer() return s_audio_utils_get_frames_per_buffer; } +jmethodID GetOnGbaFrame() +{ + return s_on_gba_frame; +} + +jclass GetGbaLibraryClass() +{ + return s_gba_library_class; +} } // namespace IDCache extern "C" { JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) + { s_java_vm = vm; @@ -816,6 +829,11 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetStaticMethodID(audio_utils_class, "getFramesPerBuffer", "()I"); env->DeleteLocalRef(audio_utils_class); + const jclass gba_library_class = env->FindClass("org/dolphinemu/dolphinemu/features/gba/GbaLibrary"); + s_gba_library_class = reinterpret_cast(env->NewGlobalRef(gba_library_class)); + s_on_gba_frame = env->GetStaticMethodID(gba_library_class, "onGbaFrame", "(I)V"); + env->DeleteLocalRef(gba_library_class); + return JNI_VERSION; } @@ -853,5 +871,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved) env->DeleteGlobalRef(s_input_detector_class); env->DeleteGlobalRef(s_permission_handler_class); env->DeleteGlobalRef(s_audio_utils_class); + env->DeleteGlobalRef(s_gba_library_class); } } diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0aaa9feec3..ad2af99560 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -124,4 +124,7 @@ jclass GetAudioUtilsClass(); jmethodID GetAudioUtilsGetSampleRate(); jmethodID GetAudioUtilsGetFramesPerBuffer(); +jclass GetGbaLibraryClass(); +jmethodID GetOnGbaFrame(); + } // namespace IDCache diff --git a/Source/Android/jni/Input/InputOverrider.cpp b/Source/Android/jni/Input/InputOverrider.cpp index 26b823e03d..f77995c80e 100644 --- a/Source/Android/jni/Input/InputOverrider.cpp +++ b/Source/Android/jni/Input/InputOverrider.cpp @@ -36,6 +36,20 @@ Java_org_dolphinemu_dolphinemu_features_input_model_InputOverrider_unregisterWii ciface::Touch::UnregisterWiiInputOverrider(controller_index); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_InputOverrider_registerGBA(JNIEnv*, jclass, + int controller_index) +{ + ciface::Touch::RegisterGBAInputOverrider(controller_index); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_InputOverrider_unregisterGBA( + JNIEnv*, jclass, int controller_index) +{ + ciface::Touch::UnregisterGBAInputOverrider(controller_index); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_input_model_InputOverrider_setControlState( JNIEnv*, jclass, int controller_index, int control, double state) diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 3855cb4c18..3d24735ad7 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -66,6 +66,14 @@ #include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" +#include +#ifdef HAS_LIBMGBA +#include "Core/HW/GBACore.h" +#include "Core/HW/SI/SI.h" +#include "Core/HW/SI/SI_Device.h" +#include "Core/HW/SI/SI_DeviceGBAEmu.h" +#endif + namespace { constexpr char DOLPHIN_TAG[] = "DolphinEmuNative"; @@ -769,4 +777,41 @@ Java_org_dolphinemu_dolphinemu_NativeLibrary_GetCurrentTitleDescriptionUnchecked return ToJString(env, description); } + +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_features_gba_GbaLibrary_copyGbaFramebuffer( + JNIEnv* env, jclass, jint slot, jobject byte_buffer) +{ +#ifdef HAS_LIBMGBA + if (slot < 0 || slot >= 4) + return JNI_FALSE; + auto core = Core::System::GetInstance().GetSerialInterface().GetGBACore(slot); + if (!core) + return JNI_FALSE; + std::shared_lock lock(core->GetVideoBufferMutex()); + const auto buffer = core->GetVideoBuffer(); + if (buffer.empty()) + return JNI_FALSE; + void* dst = env->GetDirectBufferAddress(byte_buffer); + if (!dst) + return JNI_FALSE; + jlong capacity = env->GetDirectBufferCapacity(byte_buffer); + if (static_cast(capacity) < buffer.size() * sizeof(u32)) + return JNI_FALSE; + memcpy(dst, buffer.data(), buffer.size() * sizeof(u32)); + return JNI_TRUE; +#else + return JNI_FALSE; +#endif +} + +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_gba_GbaLibrary_resetGbaCore(JNIEnv*, + jclass, + jint slot) +{ +#ifdef HAS_LIBMGBA + auto core = Core::System::GetInstance().GetSerialInterface().GetGBACore(slot); + if (core) + core->Reset(); +#endif +} } diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index b67b8a45c0..da2f264b62 100644 --- a/Source/Core/Core/HW/GBACore.cpp +++ b/Source/Core/Core/HW/GBACore.cpp @@ -38,6 +38,7 @@ #ifdef ANDROID #include "jni/AndroidCommon/AndroidCommon.h" +#include "jni/AndroidCommon/IDCache.h" #endif namespace HW::GBA @@ -245,7 +246,6 @@ bool Core::Start(u64 gc_ticks) mGameInfo info; m_core->getGameInfo(m_core, &info); m_game_title = info.title; - m_save_path = NetPlay::IsNetPlayRunning() ? NetPlay::GetGBASavePath(m_device_number) : GetSavePath(m_rom_path, m_device_number); if (!m_save_path.empty() && !LoadSave(m_save_path.c_str())) @@ -445,6 +445,26 @@ void Core::SetAudioBufferSize() m_core->setAudioBufferSize(m_core, AUDIO_BUFFER_SIZE); } + static void PushFrameReady(int slot) + { +#ifdef ANDROID + JNIEnv* env = IDCache::GetEnvForThread(); + if (!env) + return; + + env->CallStaticVoidMethod( + IDCache::GetGbaLibraryClass(), + IDCache::GetOnGbaFrame(), + static_cast(slot)); + + if (env->ExceptionCheck()) + { + env->ExceptionDescribe(); + env->ExceptionClear(); + } +#endif + } + void Core::AddCallbacks() { mCoreCallbacks callbacks{}; @@ -455,8 +475,11 @@ void Core::AddCallbacks() }; callbacks.videoFrameEnded = [](void* context) { auto core = static_cast(context); - if (auto host = core->m_host.lock()) - host->FrameEnded(core->m_video_buffer); + { + if (auto host = core->m_host.lock()) + host->FrameEnded(core->m_video_buffer); + } + PushFrameReady(core->m_device_number); }; m_core->addCoreCallbacks(m_core, &callbacks); } diff --git a/Source/Core/Core/HW/GBACore.h b/Source/Core/Core/HW/GBACore.h index 6b1934f71c..961ed68405 100644 --- a/Source/Core/Core/HW/GBACore.h +++ b/Source/Core/Core/HW/GBACore.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -91,6 +92,8 @@ public: mAudioBuffer* GetAudioBuffer() { return m_core->getAudioBuffer(m_core); } std::span GetVideoBuffer() const { return m_video_buffer; } + // android video buffer mutex. + std::shared_mutex& GetVideoBufferMutex() { return m_video_buffer_mutex; } mPlatform GetPlatform() const { return m_core->platform(m_core); } u32 GetAudioSampleRate() const { return m_core->audioSampleRate(m_core); } @@ -139,6 +142,8 @@ private: std::string m_save_path; std::array m_rom_hash{}; std::string m_game_title; + // guard for m_video_buffer vulkan, surface changes caused a hard crash to main menu. + mutable std::shared_mutex m_video_buffer_mutex; mCore* m_core{}; mCoreSync m_core_sync{}; diff --git a/Source/Core/Core/HW/SI/SI.cpp b/Source/Core/Core/HW/SI/SI.cpp index 11e0c6f2e7..7572535abf 100644 --- a/Source/Core/Core/HW/SI/SI.cpp +++ b/Source/Core/Core/HW/SI/SI.cpp @@ -31,6 +31,11 @@ #include "InputCommon/ControllerInterface/ControllerInterface.h" +#ifdef HAS_LIBMGBA +#include "Core/HW/GBACore.h" +#include "Core/HW/SI/SI_DeviceGBAEmu.h" +#endif + namespace SerialInterface { // SI Internal Hardware Addresses @@ -597,5 +602,16 @@ u32 SerialInterfaceManager::GetPollXLines() { return m_poll.X; } - +// Android GBA emulation. +std::shared_ptr SerialInterfaceManager::GetGBACore(int channel) const +{ +#ifdef HAS_LIBMGBA + if (channel < 0 || channel >= MAX_SI_CHANNELS) + return nullptr; + auto* dev = m_channel[channel].device.get(); + if (dev && dev->GetDeviceType() == SIDEVICE_GC_GBA_EMULATED) + return static_cast(dev)->GetCore(); +#endif + return nullptr; +} } // namespace SerialInterface diff --git a/Source/Core/Core/HW/SI/SI.h b/Source/Core/Core/HW/SI/SI.h index 5229bd045f..7f9b0f0741 100644 --- a/Source/Core/Core/HW/SI/SI.h +++ b/Source/Core/Core/HW/SI/SI.h @@ -24,6 +24,11 @@ namespace MMIO { class Mapping; } +// Android GBA +namespace HW::GBA +{ +class Core; +} namespace SerialInterface { @@ -68,6 +73,8 @@ public: u32 GetPollXLines(); + std::shared_ptr GetGBACore(int channel) const; + static constexpr u32 BUFFER_SIZE = 128; private: diff --git a/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h b/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h index 89a8473b4d..a62cc4839b 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h +++ b/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h @@ -31,6 +31,8 @@ public: void SendCommand(u32 command, u8 poll) override; void DoState(PointerWrap& p) override; void OnEvent(u64 userdata, s64 cycles_late) override; + // android gba core + std::shared_ptr GetCore() const { return m_core; } private: enum class NextAction diff --git a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp b/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp index 9ddbaffcd6..b33eb6ff8c 100644 --- a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp @@ -13,6 +13,8 @@ #include "Common/Assert.h" +#include "Core/HW/GBAPad.h" +#include "Core/HW/GBAPadEmu.h" #include "Core/HW/GCPad.h" #include "Core/HW/GCPadEmu.h" #include "Core/HW/Wiimote.h" @@ -145,6 +147,19 @@ const ControlsMap s_classic_controls_map = {{ ControlID::CLASSIC_RIGHT_STICK_Y}, }}; +static const ControlsMap s_gbapad_controls_map = {{ + {{GBAPad::BUTTONS_GROUP, GBAPad::A_BUTTON}, ControlID::GCPAD_A_BUTTON}, + {{GBAPad::BUTTONS_GROUP, GBAPad::B_BUTTON}, ControlID::GCPAD_B_BUTTON}, + {{GBAPad::BUTTONS_GROUP, GBAPad::L_BUTTON}, ControlID::GCPAD_L_DIGITAL}, + {{GBAPad::BUTTONS_GROUP, GBAPad::R_BUTTON}, ControlID::GCPAD_R_DIGITAL}, + {{GBAPad::BUTTONS_GROUP, GBAPad::START_BUTTON}, ControlID::GCPAD_START_BUTTON}, + {{GBAPad::BUTTONS_GROUP, GBAPad::SELECT_BUTTON}, ControlID::GCPAD_Z_BUTTON}, + {{GBAPad::DPAD_GROUP, DIRECTION_UP}, ControlID::GCPAD_DPAD_UP}, + {{GBAPad::DPAD_GROUP, DIRECTION_DOWN}, ControlID::GCPAD_DPAD_DOWN}, + {{GBAPad::DPAD_GROUP, DIRECTION_LEFT}, ControlID::GCPAD_DPAD_LEFT}, + {{GBAPad::DPAD_GROUP, DIRECTION_RIGHT}, ControlID::GCPAD_DPAD_RIGHT}, +}}; + ControllerEmu::InputOverrideFunction GetInputOverrideFunction(const ControlsMap& controls_map, size_t i) { @@ -221,6 +236,20 @@ void UnregisterWiiInputOverrider(int controller_index) s_state_arrays[controller_index][i].overriding = false; } +void RegisterGBAInputOverrider(int controller_index) +{ + Pad::GetGBAConfig() + ->GetController(controller_index) + ->SetInputOverrideFunction(GetInputOverrideFunction(s_gbapad_controls_map, controller_index)); +} + +void UnregisterGBAInputOverrider(int controller_index) +{ + Pad::GetGBAConfig()->GetController(controller_index)->ClearInputOverrideFunction(); + for (size_t i = ControlID::FIRST_GC_CONTROL; i <= ControlID::LAST_GC_CONTROL; ++i) + s_state_arrays[controller_index][i].overriding = false; +} + void SetControlState(int controller_index, ControlID control, double state) { InputState& input_state = s_state_arrays[controller_index][control]; diff --git a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.h b/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.h index 6217b44d4e..22fe7a8607 100644 --- a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.h +++ b/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.h @@ -80,6 +80,8 @@ void RegisterGameCubeInputOverrider(int controller_index); void RegisterWiiInputOverrider(int controller_index); void UnregisterGameCubeInputOverrider(int controller_index); void UnregisterWiiInputOverrider(int controller_index); +void RegisterGBAInputOverrider(int controller_index); +void UnregisterGBAInputOverrider(int controller_index); void SetControlState(int controller_index, ControlID control, double state); void ClearControlState(int controller_index, ControlID control);