This commit is contained in:
Linkinworm 2026-05-08 10:46:33 +01:00 committed by GitHub
commit 28ec1654b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1304 additions and 97 deletions

View File

@ -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()

View File

@ -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<GbaOverlayView>()
private val lastGbaTapTimes = mutableMapOf<Int, Long>()
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<String>, riivolution: Boolean, fromIntent: Boolean = false) {
fun launch(
activity: FragmentActivity,
filePaths: Array<String>,
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()

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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<GbaOverlayView> = 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<GbaOverlayView>) {
handler.removeCallbacksAndMessages(null)
attached = true
activeViews = views
views.forEach {
it.renderManager = this
if (it.surfaceReady) requestRedraw(it.gbaSlot)
}
}
fun updateViews(views: List<GbaOverlayView>) {
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
}
}

View File

@ -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)

View File

@ -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<StringSetting> =
HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY))
}

View File

@ -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

View File

@ -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<SettingsItem>,
controller: EmulatedController,
groupTypeFilter: Set<Int>?
sl: ArrayList<SettingsItem>,
controller: EmulatedController,
groupTypeFilter: Set<Int>?
) {
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)
}

View File

@ -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<InputOverlayDrawableButton> = HashSet()
private val gbaOverlayDpads: MutableSet<InputOverlayDrawableDpad> = 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<Int>()
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<InputOverlayDrawableButton>,
controllerIndex: Int,
action: Int,
event: MotionEvent,
pointerIndex: Int,
pressedIn: Boolean,
claimedPointers: MutableSet<Int> = mutableSetOf(),
excludePointers: Set<Int> = 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<InputOverlayDrawableDpad>,
controllerIndex: Int,
action: Int,
event: MotionEvent,
pointerIndex: Int,
pressedIn: Boolean,
claimedPointers: MutableSet<Int> = mutableSetOf(),
excludePointers: Set<Int> = 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<Int>()

View File

@ -29,8 +29,8 @@ import java.util.Set;
public final class FileBrowserHelper
{
public static final HashSet<String> 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<String> GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS);
@ -40,19 +40,19 @@ public final class FileBrowserHelper
}
public static final HashSet<String> 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<String> BIN_EXTENSION = new HashSet<>(Collections.singletonList(
"bin"));
"bin"));
public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
"raw"));
"raw"));
public static final HashSet<String> WAD_EXTENSION = new HashSet<>(Collections.singletonList(
"wad"));
"wad"));
public static Intent createDirectoryPickerIntent(FragmentActivity activity,
HashSet<String> extensions)
HashSet<String> 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<String> 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

View File

@ -11,7 +11,9 @@
<SurfaceView
android:id="@+id/surface_emulation"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="0dp"
android:focusable="false"
android:focusableInTouchMode="false" />

View File

@ -30,4 +30,14 @@
<item
android:id="@+id/menu_emulation_reset_overlay"
android:title="@string/emulation_touch_overlay_reset"/>
<item android:id="@+id/menu_emulation_gba_snap"
android:checkable="true"
android:title="Snap Gba Layout"/>
<item android:id="@+id/menu_emulation_gba_reset"
android:title="Reset Gba Screens"/>
<item android:id="@+id/menu_emulation_gba_reset_core"
android:title="Reset Gba"/>
</menu>

View File

@ -379,6 +379,7 @@
<item>@string/gcpad_taru_konga</item>
<item>@string/gcpad_am_baseboard</item>
<item>@string/gcpad_gc_adapter</item>
<item>@string/gba_adapter</item>
</string-array>
<integer-array name="gcpadTypeValues">
<item>0</item>
@ -389,6 +390,7 @@
<item>10</item>
<item>11</item>
<item>12</item>
<item>13</item>
</integer-array>
<string-array name="wiimoteTypeEntries">

View File

@ -89,6 +89,8 @@
<string name="gba_bios_path">BIOS</string>
<string name="gb_player_rom">Game Boy Player ROM</string>
<string name="gba_saves_path">Saves</string>
<string name="gba_rom_path">GBA ROM Path</string>
<string name="gba_rom_path_description">Rom to load for this slot.</string>
<string name="wii_submenu">Wii</string>
<string name="wii_misc_settings">Misc Settings</string>
<string name="wii_sd_card_settings">SD Card Settings</string>
@ -643,6 +645,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="gc_adapter_rumble_description">Enable the vibration function for this GameCube controller.</string>
<string name="gc_adapter_bongos">Bongo Controller</string>
<string name="gc_adapter_bongos_description">Enable this if you are using bongos on this port.</string>
<string name="gba_adapter">GBA (Integrated)</string>
<string name="path_not_changeable_scoped_storage">Due to the Scoped Storage policy in Android 11 and newer, you can\'t change this path.</string>
<string name="load_settings">Loading Settings…</string>

View File

@ -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<jclass>(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);
}
}

View File

@ -124,4 +124,7 @@ jclass GetAudioUtilsClass();
jmethodID GetAudioUtilsGetSampleRate();
jmethodID GetAudioUtilsGetFramesPerBuffer();
jclass GetGbaLibraryClass();
jmethodID GetOnGbaFrame();
} // namespace IDCache

View File

@ -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)

View File

@ -66,6 +66,14 @@
#include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/AndroidCommon/IDCache.h"
#include <shared_mutex>
#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<std::shared_mutex> 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<size_t>(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
}
}

View File

@ -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<jint>(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<Core*>(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);
}

View File

@ -7,6 +7,7 @@
#include <array>
#include <memory>
#include <shared_mutex>
#include <span>
#include <string>
#include <string_view>
@ -91,6 +92,8 @@ public:
mAudioBuffer* GetAudioBuffer() { return m_core->getAudioBuffer(m_core); }
std::span<const u32> 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<u8, 20> 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{};

View File

@ -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<HW::GBA::Core> 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<CSIDevice_GBAEmu*>(dev)->GetCore();
#endif
return nullptr;
}
} // namespace SerialInterface

View File

@ -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<HW::GBA::Core> GetGBACore(int channel) const;
static constexpr u32 BUFFER_SIZE = 128;
private:

View File

@ -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<HW::GBA::Core> GetCore() const { return m_core; }
private:
enum class NextAction

View File

@ -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];

View File

@ -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);