diff --git a/.gitattributes b/.gitattributes
index 6fd1589a1..0b6224cf0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -3,3 +3,5 @@ app/src/main/assets/imagefs.tzst filter=lfs diff=lfs merge=lfs -text
app/src/main/assets/imagefs.part*.tzst filter=lfs diff=lfs merge=lfs -text
reference/tooling/jadx.zip filter=lfs diff=lfs merge=lfs -text
reference/tooling/jadx/lib/jadx-1.5.3-all.jar filter=lfs diff=lfs merge=lfs -text
+
+app/src/main/jniLibs/arm64-v8a/*_libretro_android.so filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitmodules b/.gitmodules
index e1ec52737..4bdfced71 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,9 @@
[submodule "app/src/main/cpp/adrenotools"]
path = app/src/main/cpp/adrenotools
url = https://github.com/Pipetto-crypto/libadrenotools.git
+[submodule "libretrodroid/src/main/cpp/oboe"]
+ path = libretrodroid/src/main/cpp/oboe
+ url = https://github.com/google/oboe
+[submodule "libretrodroid/src/main/cpp/libretro/libretro-common"]
+ path = libretrodroid/src/main/cpp/libretro/libretro-common
+ url = https://github.com/libretro/libretro-common
diff --git a/README.md b/README.md
index 7f2622749..8544385fb 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,24 @@ Designed for enthusiasts and power users, WinNative delivers the full Winlator e
---
+### Retro Console Support
+
+WinNative can also run classic console games alongside your PC library. Retro games live in the same Library and launch just like PC games, but run on an embedded libretro backend instead of Wine.
+
+Supported systems (bundled cores):
+
+| System | Core | ROM extensions |
+| --- | --- | --- |
+| NES | FCEUmm | `.nes` `.unf` `.unif` |
+| SNES | Snes9x | `.smc` `.sfc` `.swc` `.fig` |
+| Game Boy / Color | Gambatte | `.gb` `.gbc` |
+| Game Boy Advance | mGBA | `.gba` |
+| Genesis / Mega Drive, Master System, Game Gear | Genesis Plus GX | `.gen` `.md` `.smd` `.sms` `.gg` |
+| Nintendo 64 | Mupen64Plus-Next | `.n64` `.z64` `.v64` |
+| PlayStation | PCSX-ReARMed | `.cue` `.chd` `.pbp` `.m3u` `.iso` |
+
+**How to use:** In the Library, tap **Add Custom Game** and select a ROM instead of an `.exe`. WinNative detects the console and adds the game to your Library. Tap **Play** to launch it with on-screen touch controls and physical gamepad support; the in-game menu (Back button or on-screen **MENU**) offers save/load state, reset, and fast-forward. PlayStation BIOS files (e.g. `scph5501.bin`) can be placed in the app's `files/retro/system` folder for better compatibility.
+
### Contributing
We welcome community contributions! Feel free to open a pull request for bug fixes, driver updates, UI improvements, or anything else you'd like to add.
@@ -56,3 +74,5 @@ Please match the existing code style and ensure any AI-assisted code is thorough
- **Pluvia** features by the [Pluvia](https://github.com/oxters168/Pluvia) / [GameNative](https://github.com/utkarshdalal/GameNative) community
- **Mesa/Turnip** contributions by the [Mesa3D](https://www.mesa3d.org/) team
- **Goldberg Steam Emulator** by [Mr. Goldberg](https://gitlab.com/Mr_Goldberg/goldberg_emulator), maintained by [Detanup01](https://github.com/Detanup01/gbe_fork)
+- **LibretroDroid** by [Filippo Scognamiglio](https://github.com/Swordfish90/LibretroDroid) (GPL-3.0) — the embedded libretro host for retro console support
+- **libretro / RetroArch** and the individual core authors (FCEUmm, Snes9x, Gambatte, mGBA, Genesis Plus GX, Mupen64Plus-Next, PCSX-ReARMed)
diff --git a/app/build.gradle b/app/build.gradle
index 9a83a7dea..768c1eef1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -280,6 +280,8 @@ dependencies {
implementation libs.playServicesGamesV2
implementation libs.workRuntimeKtx
+
+ implementation project(':libretrodroid')
}
spotless {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dcb6bc9e1..86467ead4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -127,6 +127,14 @@
android:screenOrientation="sensorLandscape"
android:exported="false" />
+
+
diff --git a/app/src/main/app/shell/LibraryGameLaunchScreen.kt b/app/src/main/app/shell/LibraryGameLaunchScreen.kt
index 66f62fb8f..801b8df6f 100644
--- a/app/src/main/app/shell/LibraryGameLaunchScreen.kt
+++ b/app/src/main/app/shell/LibraryGameLaunchScreen.kt
@@ -134,6 +134,7 @@ internal fun LibraryGameLaunchScreen(
lastPlayedMillis: Long,
installSizeText: String?,
isCustom: Boolean,
+ isRetro: Boolean = false,
hasPinnedShortcut: Boolean,
steamMenuEnabled: Boolean = false,
areSteamActionsEnabled: Boolean = true,
@@ -165,7 +166,7 @@ internal fun LibraryGameLaunchScreen(
val actionIconSize = 46.dp
val actionIconSpacing = 8.dp
// Action icons: Settings, Boot, CloudSync, Shortcut, Delete.
- val actionIconCount = 5
+ val actionIconCount = if (isRetro) 2 else 5
val actionWidth = actionIconSize * actionIconCount + actionIconSpacing * (actionIconCount - 1)
val playHeight = 56.dp
val contentGap = 18.dp
@@ -391,27 +392,29 @@ internal fun LibraryGameLaunchScreen(
size = actionIconSize,
onClick = onSettings,
)
- LaunchIconActionButton(
- icon = Icons.Outlined.DesktopWindows,
- contentDescription = stringResource(R.string.hero_boot_to_desktop_title),
- size = actionIconSize,
- onClick = onBootToDesktop,
- )
- LaunchIconActionButton(
- icon = Icons.Outlined.CloudSync,
- contentDescription = stringResource(R.string.cloud_saves_title),
- size = actionIconSize,
- onClick = onCloudSaves,
- )
- LaunchIconActionButton(
- icon = Icons.Outlined.Home,
- contentDescription =
- stringResource(
- if (hasPinnedShortcut) R.string.common_ui_remove else R.string.common_ui_shortcut,
- ),
- size = actionIconSize,
- onClick = onShortcut,
- )
+ if (!isRetro) {
+ LaunchIconActionButton(
+ icon = Icons.Outlined.DesktopWindows,
+ contentDescription = stringResource(R.string.hero_boot_to_desktop_title),
+ size = actionIconSize,
+ onClick = onBootToDesktop,
+ )
+ LaunchIconActionButton(
+ icon = Icons.Outlined.CloudSync,
+ contentDescription = stringResource(R.string.cloud_saves_title),
+ size = actionIconSize,
+ onClick = onCloudSaves,
+ )
+ LaunchIconActionButton(
+ icon = Icons.Outlined.Home,
+ contentDescription =
+ stringResource(
+ if (hasPinnedShortcut) R.string.common_ui_remove else R.string.common_ui_shortcut,
+ ),
+ size = actionIconSize,
+ onClick = onShortcut,
+ )
+ }
Box {
LaunchIconActionButton(
icon = Icons.Outlined.Delete,
diff --git a/app/src/main/app/shell/UnifiedActivity.kt b/app/src/main/app/shell/UnifiedActivity.kt
index bd99fd0a6..0a3da213e 100644
--- a/app/src/main/app/shell/UnifiedActivity.kt
+++ b/app/src/main/app/shell/UnifiedActivity.kt
@@ -94,6 +94,7 @@ import androidx.compose.ui.zIndex
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
@@ -114,6 +115,7 @@ import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
@@ -365,6 +367,8 @@ class UnifiedActivity :
var libraryPlaytimeRefreshSignal by mutableIntStateOf(0)
private var hasCompletedInitialResume = false
+ private var retroBadgeByAppId by mutableStateOf