Add Genesis emulation support with bundled Genesis Plus GX core

Bundles genesis_plus_gx_libretro_android.so (~12 MB) as an asset and
wires up GenesisCore with 3/6-button Genesis mappings (A/B/C/X/Y/Z/
Mode/Start/D-pad) following Genesis Plus GX's default retropad layout.
This commit is contained in:
2026-04-10 20:19:15 -04:00
parent bf346db085
commit 447bc20b61
5 changed files with 262 additions and 1 deletions

View File

@@ -43,6 +43,7 @@ enum class ConsoleType(
displayName = "Sega Genesis",
manufacturer = "Sega",
fileExtensions = listOf("gen", "md", "smd", "bin"),
isImplemented = true,
fallbackAspectRatio = 4f / 3f,
libretroThumbnailRepo = "Sega_-_Mega_Drive_-_Genesis"
),

View File

@@ -1,6 +1,7 @@
package com.lazy.emulate.emulation
import android.content.Context
import com.lazy.emulate.emulation.cores.GenesisCore
import com.lazy.emulate.emulation.cores.NesCore
import com.lazy.emulate.emulation.cores.PlaceholderCore
import com.lazy.emulate.emulation.cores.Ps1Core
@@ -18,7 +19,7 @@ object EmulationEngine {
ConsoleType.NES -> NesCore(context, buttonMappingManager)
ConsoleType.SNES -> SnesCore(context, buttonMappingManager)
ConsoleType.N64 -> PlaceholderCore(ConsoleType.N64)
ConsoleType.GENESIS -> PlaceholderCore(ConsoleType.GENESIS)
ConsoleType.GENESIS -> GenesisCore(context, buttonMappingManager)
ConsoleType.PS2 -> PlaceholderCore(ConsoleType.PS2)
}
}

View File

@@ -0,0 +1,242 @@
package com.lazy.emulate.emulation.cores
import android.content.Context
import android.util.Log
import android.view.Surface
import com.lazy.emulate.emulation.ConsoleType
import com.lazy.emulate.emulation.DisplayMode
import com.lazy.emulate.emulation.EmulatorCore
import com.lazy.emulate.emulation.NativeLibretro
import com.lazy.emulate.emulation.SaveSlotInfo
import com.lazy.emulate.input.ButtonMappingManager
import com.lazy.emulate.input.GamepadButton
import java.io.File
class GenesisCore(
private val context: Context,
private val buttonMappingManager: ButtonMappingManager? = null
) : EmulatorCore {
override val consoleType = ConsoleType.GENESIS
override var isRunning: Boolean = false
private set
private var buttonState: Int = 0
private val coresDir = File(context.filesDir, "cores")
private val systemDir = File(context.filesDir, "system")
private val baseSavesDir = File(context.filesDir, "saves")
private lateinit var savesDir: File
override fun loadRom(path: String): Boolean {
coresDir.mkdirs()
systemDir.mkdirs()
val romName = File(path).nameWithoutExtension
savesDir = File(baseSavesDir, romName)
savesDir.mkdirs()
NativeLibretro.nativeSetSystemDir(systemDir.absolutePath)
NativeLibretro.nativeSetSaveDir(savesDir.absolutePath)
val corePath = findCorePath()
if (corePath == null) {
Log.e(TAG, "Genesis core .so not found. Place genesis_plus_gx_libretro_android.so in ${coresDir.absolutePath}")
return false
}
if (!NativeLibretro.nativeLoadCore(corePath)) {
Log.e(TAG, "Failed to load core: $corePath")
return false
}
if (!NativeLibretro.nativeLoadGame(path)) {
Log.e(TAG, "Failed to load game: $path")
return false
}
Log.i(TAG, "Game loaded successfully: $path")
return true
}
override fun start() {
isRunning = true
NativeLibretro.nativeStart()
}
override fun pause() {
isRunning = false
NativeLibretro.nativePause()
}
override fun resume() {
isRunning = true
NativeLibretro.nativeResume()
}
override fun stop() {
isRunning = false
NativeLibretro.nativeStop()
}
override fun reset() {
NativeLibretro.nativeReset()
}
override fun setSurface(surface: Surface) {
NativeLibretro.nativeSetSurface(surface)
}
override fun onButtonPressed(button: GamepadButton) {
val bit = mapButtonToRetroId(button)
if (bit >= 0) {
buttonState = buttonState or (1 shl bit)
NativeLibretro.nativeSetButtonState(buttonState)
}
}
override fun onButtonReleased(button: GamepadButton) {
val bit = mapButtonToRetroId(button)
if (bit >= 0) {
buttonState = buttonState and (1 shl bit).inv()
NativeLibretro.nativeSetButtonState(buttonState)
}
}
override fun onAnalogStick(x: Float, y: Float, stickIndex: Int) {
// Genesis has no analog sticks
}
override fun saveState(slot: Int): Boolean {
val path = File(savesDir, "state_$slot.sav").absolutePath
return NativeLibretro.nativeSaveState(path)
}
override fun loadState(slot: Int): Boolean {
val path = File(savesDir, "state_$slot.sav").absolutePath
return NativeLibretro.nativeLoadState(path)
}
override fun deleteState(slot: Int): Boolean {
val state = File(savesDir, "state_$slot.sav")
val screenshot = File(savesDir, "state_$slot.jpg")
val deleted = state.exists() && state.delete()
if (screenshot.exists()) screenshot.delete()
return deleted
}
override fun getSaveSlotInfo(slot: Int): SaveSlotInfo {
val file = File(savesDir, "state_$slot.sav")
val screenshot = File(savesDir, "state_$slot.jpg")
return SaveSlotInfo(
slot = slot,
exists = file.exists(),
lastModified = if (file.exists()) file.lastModified() else 0L,
screenshotPath = if (screenshot.exists()) screenshot.absolutePath else null
)
}
override fun getScreenshotPath(slot: Int): String =
File(savesDir, "state_$slot.jpg").absolutePath
override fun setAudioEnabled(enabled: Boolean) {
NativeLibretro.nativeSetAudioEnabled(enabled)
}
override fun setFrameSkip(skip: Int) {
NativeLibretro.nativeSetFrameSkip(skip)
}
override fun setDisplayMode(mode: DisplayMode) {
NativeLibretro.nativeSetDisplayMode(mode.nativeValue)
}
private fun findCorePath(): String? {
val names = listOf(
"genesis_plus_gx_libretro_android.so",
"genesis_plus_gx_libretro.so",
"picodrive_libretro_android.so",
"picodrive_libretro.so",
)
// Check internal cores dir first
for (name in names) {
val file = File(coresDir, name)
if (file.exists()) return file.absolutePath
}
// Extract bundled core from assets
for (name in names) {
try {
context.assets.open("cores/$name").use { input ->
val dest = File(coresDir, name)
dest.outputStream().use { output -> input.copyTo(output) }
dest.setExecutable(true)
Log.i(TAG, "Extracted bundled core: $name")
return dest.absolutePath
}
} catch (_: Exception) {
// Asset doesn't exist, try next
}
}
// Fallback: auto-import from /sdcard/Games/cores/
val sdcardCores = File("/sdcard/Games/cores")
if (sdcardCores.exists()) {
for (name in names) {
val sdFile = File(sdcardCores, name)
if (sdFile.exists()) {
val dest = File(coresDir, name)
try {
sdFile.copyTo(dest, overwrite = true)
dest.setExecutable(true)
Log.i(TAG, "Imported core from sdcard: $name")
return dest.absolutePath
} catch (e: Exception) {
Log.e(TAG, "Failed to import core: ${e.message}")
}
}
}
}
return null
}
private fun mapButtonToRetroId(button: GamepadButton): Int {
if (buttonMappingManager != null) {
return buttonMappingManager.getRetroId(consoleType, button)
}
return defaultMapButtonToRetroId(button)
}
private fun defaultMapButtonToRetroId(button: GamepadButton): Int {
// Genesis Plus GX default retropad -> Genesis mapping:
// RETRO_Y (1) -> Genesis A
// RETRO_B (0) -> Genesis B
// RETRO_A (8) -> Genesis C
// RETRO_L (10) -> Genesis X (6-button)
// RETRO_X (9) -> Genesis Y (6-button)
// RETRO_R (11) -> Genesis Z (6-button)
// RETRO_SELECT (2) -> Mode
// RETRO_START (3) -> Start
return when (button) {
GamepadButton.FACE_BOTTOM -> 1 // Genesis A
GamepadButton.FACE_RIGHT -> 0 // Genesis B
GamepadButton.FACE_LEFT -> 8 // Genesis C
GamepadButton.FACE_TOP -> 9 // Genesis Y (6-button)
GamepadButton.L1 -> 10 // Genesis X (6-button)
GamepadButton.R1 -> 11 // Genesis Z (6-button)
GamepadButton.C_BUTTON -> 8 // alt binding for C
GamepadButton.Z_BUTTON -> 11 // alt binding for Z
GamepadButton.DPAD_UP -> 4
GamepadButton.DPAD_DOWN -> 5
GamepadButton.DPAD_LEFT -> 6
GamepadButton.DPAD_RIGHT -> 7
GamepadButton.MODE -> 2
GamepadButton.SELECT -> 2
GamepadButton.START -> 3
else -> -1
}
}
companion object {
private const val TAG = "GenesisCore"
}
}

View File

@@ -47,6 +47,23 @@ class ButtonMappingManager(private val preferencesManager: PreferencesManager) {
GamepadButton.DPAD_RIGHT to 7,
GamepadButton.SELECT to 2,
GamepadButton.START to 3
),
ConsoleType.GENESIS to mapOf(
// Genesis Plus GX default retropad -> Genesis mapping
GamepadButton.FACE_BOTTOM to 1, // Genesis A (RETRO_Y)
GamepadButton.FACE_RIGHT to 0, // Genesis B (RETRO_B)
GamepadButton.FACE_LEFT to 8, // Genesis C (RETRO_A)
GamepadButton.FACE_TOP to 9, // Genesis Y (6-button)
GamepadButton.L1 to 10, // Genesis X (6-button)
GamepadButton.R1 to 11, // Genesis Z (6-button)
GamepadButton.C_BUTTON to 8, // alt C
GamepadButton.Z_BUTTON to 11, // alt Z
GamepadButton.DPAD_UP to 4,
GamepadButton.DPAD_DOWN to 5,
GamepadButton.DPAD_LEFT to 6,
GamepadButton.DPAD_RIGHT to 7,
GamepadButton.MODE to 2, // Genesis Mode
GamepadButton.START to 3
)
)