From 34af99ad26cbddfdb88f326855a9c5b89722e281 Mon Sep 17 00:00:00 2001 From: Linkinworm Date: Fri, 17 Apr 2026 21:39:34 +0100 Subject: [PATCH] Enables, GBA on android. links to games, can be played on its own. --- .../dolphinemu/dolphinemu/NativeLibrary.kt | 31 +- .../activities/EmulationActivity.kt | 443 +++++++++++- .../dolphinemu/features/gba/GBAOverlayView.kt | 80 +++ .../features/gba/GbaRenderManager.kt | 70 ++ .../features/input/model/InputOverrider.kt | 4 + .../features/settings/model/BooleanSetting.kt | 21 +- .../features/settings/model/StringSetting.kt | 12 + .../features/settings/ui/SettingsAdapter.kt | 27 + .../settings/ui/SettingsFragmentPresenter.kt | 49 +- .../dolphinemu/overlay/InputOverlay.kt | 447 +++++++++++- .../dolphinemu/utils/FileBrowserHelper.java | 32 +- .../main/res/layout/fragment_emulation.xml | 4 +- .../res/menu/menu_overlay_controls_gc.xml | 11 + .../app/src/main/res/values/arrays.xml | 668 +++++++++--------- .../app/src/main/res/values/strings.xml | 2 + Source/Android/jni/AndroidCommon/IDCache.cpp | 7 +- Source/Android/jni/AndroidCommon/IDCache.h | 2 + Source/Android/jni/Input/InputOverrider.cpp | 14 + Source/Android/jni/MainAndroid.cpp | 56 ++ Source/Core/Core/HW/GBACore.cpp | 89 ++- Source/Core/Core/HW/GBACore.h | 9 + Source/Core/Core/HW/SI/SI.cpp | 52 ++ Source/Core/Core/HW/SI/SI.h | 10 + Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp | 9 + Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h | 5 + .../Touch/InputOverrider.cpp | 29 + .../Touch/InputOverrider.h | 2 + 27 files changed, 1795 insertions(+), 390 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GBAOverlayView.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GbaRenderManager.kt 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 0edc0f91f9..3dfde3e4a3 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 @@ -13,11 +13,13 @@ import androidx.core.content.ContextCompat import androidx.core.util.Pair import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.dialogs.AlertMessage +import org.dolphinemu.dolphinemu.features.gba.GbaRenderManager import org.dolphinemu.dolphinemu.utils.CompressCallback import org.dolphinemu.dolphinemu.utils.Log import java.lang.ref.WeakReference import java.util.concurrent.Semaphore + /** * Class which contains methods that interact * with the native side of the Dolphin code. @@ -366,11 +368,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() @@ -450,6 +452,25 @@ object NativeLibrary { @JvmStatic external fun GetGameAspectRatio(): Float + @JvmStatic + external fun copyGBAFramebuffer(slot: Int, buffer: java.nio.ByteBuffer): + Boolean + + @JvmStatic + external fun resetGBACore(slot: Int) + + @JvmStatic + external fun getGBAGameTitle(slot: Int): String + + @JvmStatic + external fun getGBAGameCode(slot: Int): String + + @Keep + @JvmStatic + fun onGBAFrame(slot: Int) { + GbaRenderManager.onFrame(slot) + } + @JvmStatic fun IsEmulatingWii(): Boolean { checkGameMetadataValid() 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..43c6c178d2 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,15 @@ 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.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 +80,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 +200,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 +231,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 != 13) 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 +384,25 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { emulationFragment?.refreshInputOverlay() updateDisplaySettings() + + val activeSlots = (0 until 4).filter { + IntSetting.getSettingForSIDevice(it).int == 13 + } + 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 +411,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 +464,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 +547,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 +576,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { item.isChecked = !item.isChecked toggleRecenter(item.isChecked) } + + MENU_ACTION_GBA_SNAP -> { + item.isChecked = !item.isChecked + toggleGBASnap() + } } } @@ -518,6 +628,13 @@ 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 +650,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 +700,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 +717,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { emulationFragment?.refreshInputOverlay() } } + InputOverlay.OVERLAY_WIIMOTE_NUNCHUK -> { val nunchukLatchingButtons = BooleanArray(9) val nunchukSettingBase = "MAIN_BUTTON_LATCHING_WII_" @@ -603,21 +735,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 +1070,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 +1115,20 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider { if (anyMenuClosed) return true } + // gbas were not dragable this fixes it, but once touched they appear over the menus + 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 +1170,249 @@ 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 + + // Bring the window to the top layer + v.bringToFront() + } + + 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 == 13) + NativeLibrary.resetGBACore(slot) + } + } + + companion object { private const val BACKSTACK_NAME_MENU = "menu" private const val BACKSTACK_NAME_SUBMENU = "submenu" @@ -1077,6 +1474,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 +1490,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 +1536,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/GBAOverlayView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/gba/GBAOverlayView.kt new file mode 100644 index 0000000000..5bc85f7ce6 --- /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..2a60f09363 --- /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 (NativeLibrary.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/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index b6a03e6b61..28a9376959 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -15,7 +15,12 @@ enum class BooleanSetting( MAIN_DSP_HLE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "DSPHLE", true), MAIN_FASTMEM(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "Fastmem", true), MAIN_FASTMEM_ARENA(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "FastmemArena", true), - MAIN_LARGE_ENTRY_POINTS_MAP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "LargeEntryPointsMap", true), + MAIN_LARGE_ENTRY_POINTS_MAP( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_CORE, + "LargeEntryPointsMap", + true + ), MAIN_CPU_THREAD(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "CPUThread", true), MAIN_SYNC_ON_SKIP_IDLE( Settings.FILE_DOLPHIN, @@ -31,7 +36,12 @@ enum class BooleanSetting( false ), MAIN_AUDIO_FILL_GAPS(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "AudioFillGaps", true), - MAIN_AUDIO_PRESERVE_PITCH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "AudioPreservePitch", false), + MAIN_AUDIO_PRESERVE_PITCH( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_CORE, + "AudioPreservePitch", + false + ), MAIN_BBA_XLINK_CHAT_OSD( Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, @@ -137,7 +147,12 @@ enum class BooleanSetting( "EnableSaveStates", false ), - MAIN_WII_WIILINK_ENABLE(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "EnableWiiLink", false), + MAIN_WII_WIILINK_ENABLE( + Settings.FILE_DOLPHIN, + Settings.SECTION_INI_CORE, + "EnableWiiLink", + false + ), MAIN_DSP_JIT(Settings.FILE_DOLPHIN, Settings.SECTION_INI_DSP, "EnableJIT", true), MAIN_TIME_TRACKING( Settings.FILE_DOLPHIN, 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..2fe0324aa2 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 @@ -71,47 +71,58 @@ class SettingsAdapter( ListItemHeaderBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder( ListItemSettingSwitchBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_STRING_SINGLE_CHOICE, SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_SLIDER -> SliderViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this, context ) + SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder( ListItemSubmenuBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder( ListItemMappingBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_FILE_PICKER, SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this, context ) + SettingsItem.TYPE_STRING -> InputStringSettingViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder( ListItemHeaderBinding.inflate(inflater, parent, false), this ) + SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder( ListItemSettingBinding.inflate(inflater, parent, false), this ) + else -> throw IllegalArgumentException("Invalid view type: $viewType") } } @@ -251,6 +262,7 @@ class SettingsAdapter( slider.valueTo = item.max slider.stepSize = item.stepSize } + is IntSliderSetting -> { slider.valueFrom = item.min.toFloat() slider.valueTo = item.max.toFloat() @@ -446,6 +458,17 @@ class SettingsAdapter( fun onFilePickerConfirmation(selectedFile: String) { val filePicker = clickedItem as FilePicker + if (selectedFile.startsWith("content://")) { + try { + val uri = android.net.Uri.parse(selectedFile) + fragmentActivity.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } catch (e: SecurityException) { + } + } + if (filePicker.getSelectedValue() != selectedFile) { notifyItemChanged(clickedPosition) fragmentView.onSettingChanged() @@ -476,6 +499,7 @@ class SettingsAdapter( closeDialog() } + is SingleChoiceSettingDynamicDescriptions -> { val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions @@ -486,6 +510,7 @@ class SettingsAdapter( closeDialog() } + is StringSingleChoiceSetting -> { val scSetting = clickedItem as StringSingleChoiceSetting @@ -496,6 +521,7 @@ class SettingsAdapter( closeDialog() } + is IntSliderSetting -> { val sliderSetting = clickedItem as IntSliderSetting if (sliderSetting.selectedValue != seekbarProgress.toInt()) { @@ -504,6 +530,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 4603411c52..80e9f81669 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) } } @@ -2239,7 +2239,7 @@ class SettingsFragmentPresenter( BooleanSetting.MAIN_DEBUG_JIT_ENABLE_PROFILING, R.string.debug_jit_enable_block_profiling, 0 - ) + ) ) sl.add( RunRunnable( @@ -2405,6 +2405,7 @@ class SettingsFragmentPresenter( addControllerMappingSettings(sl, gcPad, null) } } + 7 -> { // Emulated keyboard controller val gcKeyboard = EmulatedController.getGcKeyboard(gcPadNumber) @@ -2417,6 +2418,7 @@ class SettingsFragmentPresenter( addControllerMappingSettings(sl, gcKeyboard, null) } } + 12 -> { // Adapter sl.add( @@ -2436,6 +2438,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 + ) + ) + } } } @@ -2635,11 +2652,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) } /** @@ -2734,7 +2751,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..752b0aedb1 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,7 +173,7 @@ 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 @@ -157,7 +192,11 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex button.setPressedState(if (button.latching) !button.getPressedState() else true) button.trackId = event.getPointerId(pointerIndex) pressed = true - 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) @@ -175,7 +214,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) @@ -301,6 +344,118 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex ) } + //gba touch handling + if (gbaControllerIndex >= 0) { + for (button in gbaOverlayButtons) { + when (action) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + if (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) + pressed = true + InputOverrider.setControlState( + gbaControllerIndex, + button.control, + if (button.getPressedState()) 1.0 else 0.0 + ) + val analog = getAnalogControlForTrigger(button.control) + if (analog >= 0) + InputOverrider.setControlState(gbaControllerIndex, analog, 1.0) + } + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + if (button.trackId == event.getPointerId(pointerIndex)) { + if (!button.latching) + button.setPressedState(false) + InputOverrider.setControlState( + gbaControllerIndex, + button.control, + if (button.getPressedState()) 1.0 else 0.0 + ) + val analog = getAnalogControlForTrigger(button.control) + if (analog >= 0) + InputOverrider.setControlState(gbaControllerIndex, analog, 0.0) + button.trackId = -1 + } + } + } + } + + for (dpad in gbaOverlayDpads) { + when (action) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + if (dpad.bounds.contains( + event.getX(pointerIndex).toInt(), + event.getY(pointerIndex).toInt() + ) + ) { + dpad.trackId = event.getPointerId(pointerIndex) + pressed = true + } + } + + MotionEvent.ACTION_MOVE -> { + if (dpad.trackId == event.getPointerId(pointerIndex)) { + val up = event.getY(pointerIndex) < dpad.bounds.centerY() + val down = event.getY(pointerIndex) > dpad.bounds.centerY() + val left = event.getX(pointerIndex) < dpad.bounds.centerX() + val right = event.getX(pointerIndex) > dpad.bounds.centerX() + setDpadState(dpad, up, down, left, right) + InputOverrider.setControlState( + gbaControllerIndex, + ControlId.GCPAD_DPAD_UP, if (up) 1.0 else 0.0 + ) + InputOverrider.setControlState( + gbaControllerIndex, + ControlId.GCPAD_DPAD_DOWN, if (down) 1.0 else 0.0 + ) + InputOverrider.setControlState( + gbaControllerIndex, + ControlId.GCPAD_DPAD_LEFT, if (left) 1.0 else 0.0 + ) + InputOverrider.setControlState( + gbaControllerIndex, + ControlId.GCPAD_DPAD_RIGHT, if (right) 1.0 else 0.0 + ) + } + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + if (dpad.trackId == event.getPointerId(pointerIndex)) { + dpad.trackId = -1 + dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT) + InputOverrider.setControlState( + gbaControllerIndex, + ControlId.GCPAD_DPAD_UP, + 0.0 + ) + InputOverrider.setControlState( + gbaControllerIndex, + ControlId.GCPAD_DPAD_DOWN, + 0.0 + ) + InputOverrider.setControlState( + gbaControllerIndex, + ControlId.GCPAD_DPAD_LEFT, + 0.0 + ) + InputOverrider.setControlState( + gbaControllerIndex, + ControlId.GCPAD_DPAD_RIGHT, + 0.0 + ) + } + } + } + } + } + + invalidate() return true @@ -425,6 +580,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 +668,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 +873,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 +1344,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 +1399,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex wiiOnlyPortraitDefaultOverlay() } } + if (isLandscape) gbaDefaultOverlay() else gbaPortraitDefaultOverlay() refreshControls() } @@ -1401,6 +1716,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 +2607,112 @@ 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() + } + + companion object { const val OVERLAY_GAMECUBE = 0 const val OVERLAY_WIIMOTE = 1 @@ -2287,6 +2724,10 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex private const val EMULATED_GAMECUBE_CONTROLLER = 6 private const val EMULATED_AM_BASEBOARD = 11 private const val GAMECUBE_ADAPTER = 12 + private const val EMULATED_GBA_CONTROLLER = 13 + + //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..7d2061fff3 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,15 @@ + + + + + + + diff --git a/Source/Android/app/src/main/res/values/arrays.xml b/Source/Android/app/src/main/res/values/arrays.xml index d919edb1d2..74ee53cb09 100644 --- a/Source/Android/app/src/main/res/values/arrays.xml +++ b/Source/Android/app/src/main/res/values/arrays.xml @@ -2,78 +2,37 @@ - - NTSC-J - NTSC-U - PAL - NTSC-K - 0 1 2 3 - - - - @string/jit_recompiler_x86 - @string/cached_interpreter_slower - @string/interpreter_slowest - 1 5 0 - - @string/jit_recompiler_arm64 - @string/cached_interpreter_slower - @string/interpreter_slowest - + + 4 5 0 - - @string/cached_interpreter - @string/interpreter - 5 0 - - - - @string/dsp_hle - @string/dsp_lle_recompiler - @string/dsp_lle_interpreter - 0 1 2 - - @string/dsp_hle - @string/dsp_lle_interpreter - 0 2 - - - - @string/language_english - @string/language_german - @string/language_french - @string/language_spanish - @string/language_italian - @string/language_dutch - 0 1 @@ -82,14 +41,6 @@ 4 5 - - - - @string/device_nothing - @string/device_dummy - @string/device_memory_card - @string/device_gci_folder - 255 0 @@ -97,16 +48,7 @@ 8 - - - @string/device_nothing - @string/device_dummy - @string/broadband_adapter_xlink - @string/broadband_adapter_hle - @string/broadband_adapter_tapserver - @string/modem_adapter_tapserver - @string/sp1_am_baseboard - + 255 0 @@ -116,20 +58,6 @@ 13 6 - - - - @string/language_japanese - @string/language_english - @string/language_german - @string/language_french - @string/language_spanish - @string/language_italian - @string/language_dutch - @string/language_simplified_chinese - @string/language_traditional_chinese - @string/language_korean - 0 1 @@ -142,49 +70,23 @@ 8 9 - - - - @string/sound_mode_mono - @string/sound_mode_stereo - @string/sound_mode_surround - 0 1 2 - - - - @string/sensor_position_top - @string/sensor_position_bottom - 1 0 - - - @string/log_notice - @string/log_error - @string/log_warning - @string/log_info - + 1 2 3 4 - - @string/log_notice - @string/log_error - @string/log_warning - @string/log_info - @string/log_debug - 1 2 @@ -193,29 +95,7 @@ 5 - - - @string/backend_opengl - @string/backend_vulkan - @string/backend_software - @string/backend_null - - - OGL - Vulkan - Software Renderer - Null - - - - - @string/extension_none - @string/extension_nunchuk - @string/extension_classic - @string/extension_guitar - @string/extension_drums - @string/extension_turntable - + 0 1 @@ -224,40 +104,19 @@ 4 5 - - - - @string/accuracy_fast - @string/accuracy_medium - @string/accuracy_safe - 128 512 0 - - - @string/shader_compilation_specialized - @string/shader_compilation_exclusive_ubershaders - @string/shader_compilation_hybrid_ubershaders - @string/shader_compilation_skip_drawing - + 0 1 2 3 - - - - @string/shader_compilation_specialized_description - @string/shader_compilation_exclusive_ubershaders_description - @string/shader_compilation_hybrid_ubershaders_description - @string/shader_compilation_skip_drawing_description - 0 1 @@ -265,15 +124,7 @@ 3 - - - @string/resolution_one_native - @string/resolution_two_native - @string/resolution_three_native - @string/resolution_four_native - @string/resolution_five_native - @string/resolution_six_native - + 1 2 @@ -282,14 +133,6 @@ 5 6 - - - - @string/multiple_off - @string/multiple_two - @string/multiple_four - @string/multiple_eight - 1 2 @@ -297,14 +140,7 @@ 8 - - - @string/filtering_default - @string/multiple_two - @string/multiple_four - @string/multiple_eight - @string/multiple_sixteen - + 0 1 @@ -312,40 +148,19 @@ 3 4 - - - - @string/filtering_default - @string/filtering_nearest - @string/filtering_linear - 0 1 2 - - - @string/stereoscopy_off - @string/stereoscopy_side_by_side - @string/stereoscopy_top_and_bottom - @string/stereoscopy_anaglyph - + 0 1 2 3 - - - - @string/aspect_ratio_auto - @string/aspect_ratio_force_sixteen_by_nine - @string/aspect_ratio_force_four_by_three - @string/aspect_ratio_stretch - 0 1 @@ -353,6 +168,329 @@ 3 + + + 0 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + + + 0 + 1 + 2 + + + 0 + 8 + 1 + -1 + + + 0 + 2 + 3 + 4 + + + + + 0 + 1 + 2 + 3 + 4 + + + -1 + 1 + 2 + + + + + 0 + 1 + 2 + + + 0 + 3 + 7 + 8 + + + + + 32768 + + + 2097152 + + + + + 32768 + 65536 + 131072 + 262144 + 524288 + 1048576 + 2097152 + + + 0 + + + + + 0 + 1 + 2 + 3 + 4 + + + 0 + 2 + 3 + 4 + 5 + 6 + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + + + + + 0 + 1 + 2 + + + NTSC-J + NTSC-U + PAL + NTSC-K + + + + + @string/jit_recompiler_x86 + @string/cached_interpreter_slower + @string/interpreter_slowest + + + @string/jit_recompiler_arm64 + @string/cached_interpreter_slower + @string/interpreter_slowest + + + + + @string/cached_interpreter + @string/interpreter + + + @string/dsp_hle + @string/dsp_lle_recompiler + @string/dsp_lle_interpreter + + + + + @string/dsp_hle + @string/dsp_lle_interpreter + + + @string/language_english + @string/language_german + @string/language_french + @string/language_spanish + @string/language_italian + @string/language_dutch + + + + + @string/device_nothing + @string/device_dummy + @string/device_memory_card + @string/device_gci_folder + + + @string/device_nothing + @string/device_dummy + @string/broadband_adapter_xlink + @string/broadband_adapter_hle + @string/broadband_adapter_tapserver + @string/modem_adapter_tapserver + @string/sp1_am_baseboard + + + + @string/language_japanese + @string/language_english + @string/language_german + @string/language_french + @string/language_spanish + @string/language_italian + @string/language_dutch + @string/language_simplified_chinese + @string/language_traditional_chinese + @string/language_korean + + + + @string/sound_mode_mono + @string/sound_mode_stereo + @string/sound_mode_surround + + + @string/sensor_position_top + @string/sensor_position_bottom + + + + @string/log_notice + @string/log_error + @string/log_warning + @string/log_info + + + @string/log_notice + @string/log_error + @string/log_warning + @string/log_info + @string/log_debug + + + + @string/backend_opengl + @string/backend_vulkan + @string/backend_software + @string/backend_null + + + + OGL + Vulkan + Software Renderer + Null + + + + @string/extension_none + @string/extension_nunchuk + @string/extension_classic + @string/extension_guitar + @string/extension_drums + @string/extension_turntable + + + + @string/accuracy_fast + @string/accuracy_medium + @string/accuracy_safe + + + + @string/shader_compilation_specialized + @string/shader_compilation_exclusive_ubershaders + @string/shader_compilation_hybrid_ubershaders + @string/shader_compilation_skip_drawing + + + + @string/shader_compilation_specialized_description + @string/shader_compilation_exclusive_ubershaders_description + @string/shader_compilation_hybrid_ubershaders_description + @string/shader_compilation_skip_drawing_description + + + + @string/resolution_one_native + @string/resolution_two_native + @string/resolution_three_native + @string/resolution_four_native + @string/resolution_five_native + @string/resolution_six_native + + + + @string/multiple_off + @string/multiple_two + @string/multiple_four + @string/multiple_eight + + + + @string/filtering_default + @string/multiple_two + @string/multiple_four + @string/multiple_eight + @string/multiple_sixteen + + + + @string/filtering_default + @string/filtering_nearest + @string/filtering_linear + + + + @string/stereoscopy_off + @string/stereoscopy_side_by_side + @string/stereoscopy_top_and_bottom + @string/stereoscopy_anaglyph + + + + @string/aspect_ratio_auto + @string/aspect_ratio_force_sixteen_by_nine + @string/aspect_ratio_force_four_by_three + @string/aspect_ratio_stretch + @string/country_europe @string/country_japan @@ -370,6 +508,7 @@ @string/country_unknown + @string/gcpad_disabled @string/gcpad_emulated @@ -379,29 +518,13 @@ @string/gcpad_taru_konga @string/gcpad_am_baseboard @string/gcpad_gc_adapter + GBA - - 0 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - - @string/wiimote_disabled @string/wiimote_emulated @string/wiimote_real - - 0 - 1 - 2 - - A B @@ -415,7 +538,6 @@ @string/gamepad_main_stick @string/gamepad_c_stick - A B @@ -437,7 +559,6 @@ @string/gamepad_home @string/gamepad_d_pad - A B @@ -461,7 +582,6 @@ Z @string/gamepad_nunchuk_stick - A B @@ -504,7 +624,6 @@ ZL ZR - @string/ir_disabled @string/ir_follow @@ -516,7 +635,6 @@ @string/double_tap_b @string/double_tap_2 - @string/double_tap_a @string/double_tap_b @@ -530,26 +648,13 @@ @string/orientation_portrait @string/orientation_auto - - 0 - 8 - 1 - -1 - - - @string/theme_default @string/theme_material_default @string/theme_green @string/theme_pink - - 0 - 2 - 3 - 4 - + @string/theme_default @string/theme_material_you @@ -557,36 +662,17 @@ @string/theme_green @string/theme_pink - - 0 - 1 - 2 - 3 - 4 - - @string/theme_mode_follow_system @string/theme_mode_light @string/theme_mode_dark - - -1 - 1 - 2 - @string/sync_gpu_never @string/sync_gpu_idle @string/sync_gpu_always - - 0 - 1 - 2 - - @string/motion_device_with_pointer @string/motion_device_without_pointer @@ -599,27 +685,13 @@ WIA RVZ - - 0 - 3 - 7 - 8 - - @string/block_size_32_kib - - 32768 - @string/block_size_2_mib - - 2097152 - - @string/block_size_32_kib @string/block_size_64_kib @@ -629,23 +701,10 @@ @string/block_size_1_mib @string/block_size_2_mib - - 32768 - 65536 - 131072 - 262144 - 524288 - 1048576 - 2097152 - @string/compression_deflate - - 0 - - @string/compression_none @string/compression_purge @@ -653,13 +712,6 @@ @string/compression_lzma @string/compression_lzma2 - - 0 - 1 - 2 - 3 - 4 - @string/compression_none @@ -668,15 +720,6 @@ @string/compression_lzma2 @string/compression_zstandard - - 0 - 2 - 3 - 4 - 5 - 6 - - 1 2 @@ -688,17 +731,6 @@ 8 9 - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 1 @@ -724,39 +756,9 @@ 21 22 - - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21 - 22 - - @string/ntscm_space @string/ntscj_space @string/pal_space - - 0 - 1 - 2 - diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 5c2e978d48..eb78c775ea 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. Long-press to clear Wii Misc Settings SD Card Settings diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 18b486023a..c77e623199 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -150,6 +150,11 @@ JNIEnv* GetEnvForThread() return owned.env; } +JavaVM* GetJavaVM() +{ + return s_java_vm; +} + jclass GetStringClass() { return s_string_class; @@ -574,12 +579,12 @@ jmethodID GetAudioUtilsGetFramesPerBuffer() { return s_audio_utils_get_frames_per_buffer; } - } // namespace IDCache extern "C" { JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) + { s_java_vm = vm; diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0aaa9feec3..e6b4db6911 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -124,4 +124,6 @@ jclass GetAudioUtilsClass(); jmethodID GetAudioUtilsGetSampleRate(); jmethodID GetAudioUtilsGetFramesPerBuffer(); +JavaVM* GetJavaVM(); + } // namespace IDCache diff --git a/Source/Android/jni/Input/InputOverrider.cpp b/Source/Android/jni/Input/InputOverrider.cpp index 26b823e03d..c7c7b6f0d7 100644 --- a/Source/Android/jni/Input/InputOverrider.cpp +++ b/Source/Android/jni/Input/InputOverrider.cpp @@ -35,6 +35,20 @@ Java_org_dolphinemu_dolphinemu_features_input_model_InputOverrider_unregisterWii { ciface::Touch::UnregisterWiiInputOverrider(controller_index); } +// Android GBA emu. +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( diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index 7112833403..5f251b6223 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -69,6 +69,14 @@ #include "jni/AndroidCommon/IDCache.h" #include "jni/Host.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"; @@ -815,4 +823,52 @@ Java_org_dolphinemu_dolphinemu_NativeLibrary_GetCurrentTitleDescriptionUnchecked return ToJString(env, description); } +// Android Gba Emulation. +JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_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_NativeLibrary_resetGBACore(JNIEnv*, jclass, + jint slot) +{ +#ifdef HAS_LIBMGBA + Core::System::GetInstance().GetSerialInterface().ResetGBACore(slot); +#endif +} + +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_getGBAGameTitle(JNIEnv* env, + jclass, + jint slot) +{ + return ToJString(env, Core::System::GetInstance().GetSerialInterface().GetGBAGameTitle(slot)); +} + +JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_getGBAGameCode(JNIEnv* env, + jclass, + jint slot) +{ + return ToJString(env, Core::System::GetInstance().GetSerialInterface().GetGBAGameCode(slot)); +} } diff --git a/Source/Core/Core/HW/GBACore.cpp b/Source/Core/Core/HW/GBACore.cpp index 0e42a3ec66..190aa0e113 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,6 +246,8 @@ bool Core::Start(u64 gc_ticks) mGameInfo info; m_core->getGameInfo(m_core, &info); m_game_title = info.title; + // android region code furture use. + m_game_code = std::string(info.code, 4); m_save_path = NetPlay::IsNetPlayRunning() ? NetPlay::GetGBASavePath(m_device_number) : GetSavePath(m_rom_path, m_device_number); @@ -295,6 +298,9 @@ void Core::Stop() m_save_path = {}; m_rom_hash = {}; m_game_title = {}; + // android future use, game code match. + m_game_title = {}; + m_game_code = {}; } void Core::Reset() @@ -374,12 +380,18 @@ bool Core::LoadBIOS(const char* bios_path) bool Core::LoadSave(const char* save_path) { VFile* vf = VFileOpen(save_path, O_CREAT | O_RDWR); + std::string path_str = save_path; if (!vf) { PanicAlertFmtT("Error: GBA{0} failed to open the save in {1}", m_device_number + 1, save_path); return false; } +#ifdef ANDROID + if (path_str.starts_with("content://")) + vf = VFileFromFD(OpenAndroidContent(save_path, "rw")); +#endif + if (!m_core->loadSave(m_core, vf)) { PanicAlertFmtT("Error: GBA{0} failed to load the save in {1}", m_device_number + 1, save_path); @@ -445,6 +457,75 @@ void Core::SetAudioBufferSize() m_core->setAudioBufferSize(m_core, AUDIO_BUFFER_SIZE); } +// Android GBAcore emu +#ifdef ANDROID +#include +namespace +{ +static std::atomic s_on_gba_frame{nullptr}; +struct GBAThreadJNI +{ + JNIEnv* env = nullptr; + bool attached = false; + + GBAThreadJNI() + { + JavaVM* vm = IDCache::GetJavaVM(); + if (!vm) + return; + jint rc = vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + if (rc == JNI_EDETACHED) + { + if (vm->AttachCurrentThread(&env, nullptr) != JNI_OK) + env = nullptr; + else + attached = true; + } + else if (rc != JNI_OK) + { + env = nullptr; + return; + } + if (s_on_gba_frame.load() == nullptr) + { + jclass cls = IDCache::GetNativeLibraryClass(); + if (cls) + { + jmethodID mid = env->GetStaticMethodID(cls, "onGBAFrame", "(I)V"); + if (mid) + { + s_on_gba_frame.store(mid); + } + else + { + env->ExceptionClear(); + } + } + } + } +}; +} // namespace +#endif +static void GBACore_PushFrameReady(int slot) +{ +#ifdef ANDROID + + thread_local GBAThreadJNI tls; + + jmethodID methodId = s_on_gba_frame.load(); + if (!tls.env || !methodId) + return; + tls.env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), s_on_gba_frame, + static_cast(slot)); + + if (tls.env->ExceptionCheck()) + { + tls.env->ExceptionDescribe(); + tls.env->ExceptionClear(); + } +#endif +} + void Core::AddCallbacks() { mCoreCallbacks callbacks{}; @@ -455,8 +536,12 @@ 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); + { + std::unique_lock lock(core->m_video_buffer_mutex); + if (auto host = core->m_host.lock()) + host->FrameEnded(core->m_video_buffer); + } + GBACore_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 3427f8eb0f..891866640b 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 @@ -90,6 +91,10 @@ public: mAudioBuffer* GetAudioBuffer() { return m_core->getAudioBuffer(m_core); } std::span GetVideoBuffer() const { return m_video_buffer; } + // Android game code future use. + std::string GetGameCode() const { return m_game_code; } + // android video bufffer 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 +144,10 @@ private: std::string m_save_path; std::array m_rom_hash{}; std::string m_game_title; + // android game code future use. + std::string m_game_code; + // guard for m_video_buffer vulkan + 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..f033aa8f9c 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,52 @@ 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; +} + +void SerialInterfaceManager::ResetGBACore(int channel) +{ +#ifdef HAS_LIBMGBA + if (channel < 0 || channel >= MAX_SI_CHANNELS) + return; + auto* dev = m_channel[channel].device.get(); + if (dev && dev->GetDeviceType() == SIDEVICE_GC_GBA_EMULATED) + static_cast(dev)->GetCore()->Reset(); +#endif +} + +std::string SerialInterfaceManager::GetGBAGameTitle(int channel) const +{ +#ifdef HAS_LIBMGBA + if (channel < 0 || channel >= MAX_SI_CHANNELS) + return ""; + auto* dev = m_channel[channel].device.get(); + if (dev && dev->GetDeviceType() == SIDEVICE_GC_GBA_EMULATED) + return static_cast(dev)->GetGBAGameTitle(); +#endif + return ""; +} + +std::string SerialInterfaceManager::GetGBAGameCode(int channel) const +{ +#ifdef HAS_LIBMGBA + if (channel < 0 || channel >= MAX_SI_CHANNELS) + return ""; + auto* dev = m_channel[channel].device.get(); + if (dev && dev->GetDeviceType() == SIDEVICE_GC_GBA_EMULATED) + return static_cast(dev)->GetGBAGameCode(); +#endif + return ""; +} } // namespace SerialInterface diff --git a/Source/Core/Core/HW/SI/SI.h b/Source/Core/Core/HW/SI/SI.h index 5229bd045f..4a8b2895ae 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,11 @@ public: u32 GetPollXLines(); + std::shared_ptr GetGBACore(int channel) const; + void ResetGBACore(int channel); + std::string GetGBAGameTitle(int channel) const; + std::string GetGBAGameCode(int channel) const; + static constexpr u32 BUFFER_SIZE = 128; private: diff --git a/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp b/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp index 36ccc15bc0..81402033df 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp +++ b/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.cpp @@ -172,5 +172,14 @@ void CSIDevice_GBAEmu::OnEvent(u64 userdata, s64 cycles_late) const auto num_cycles = userdata + GetSyncInterval(m_system.GetSystemTimers()); m_system.GetSerialInterface().ScheduleEvent(m_device_number, num_cycles); } +// android gameID furture use +std::string CSIDevice_GBAEmu::GetGBAGameTitle() const +{ + return m_core ? m_core->GetCoreInfo().game_title : ""; +} +std::string CSIDevice_GBAEmu::GetGBAGameCode() const +{ + return m_core ? m_core->GetGameCode() : ""; +} } // namespace SerialInterface #endif // HAS_LIBMGBA diff --git a/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h b/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h index 89a8473b4d..01647bb468 100644 --- a/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h +++ b/Source/Core/Core/HW/SI/SI_DeviceGBAEmu.h @@ -31,6 +31,11 @@ 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; } + // android gamecode match for future use. + std::string GetGBAGameTitle() const; + std::string GetGBAGameCode() const; private: enum class NextAction diff --git a/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp b/Source/Core/InputCommon/ControllerInterface/Touch/InputOverrider.cpp index 9ddbaffcd6..a7458d4df7 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" @@ -144,6 +146,19 @@ const ControlsMap s_classic_controls_map = {{ {{WiimoteEmu::Classic::RIGHT_STICK_GROUP, ControllerEmu::ReshapableInput::Y_INPUT_OVERRIDE}, ControlID::CLASSIC_RIGHT_STICK_Y}, }}; +// android GBA controls map +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) @@ -220,6 +235,20 @@ void UnregisterWiiInputOverrider(int controller_index) for (size_t i = ControlID::FIRST_WII_CONTROL; i <= ControlID::LAST_WII_CONTROL; ++i) s_state_arrays[controller_index][i].overriding = false; } +// android gba overrider +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) { 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);