mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55f34ee8d6 | ||
|
|
24b4abdfd4 | ||
|
|
fcbbe87dde | ||
|
|
d0c6998b00 | ||
|
|
8ebe501f34 | ||
|
|
699ead23f2 | ||
|
|
d64859fb72 | ||
|
|
9daf41e18f | ||
|
|
a9157db693 | ||
|
|
b07a2d2842 | ||
|
|
be7a50af46 | ||
|
|
9a1aee75d5 |
No files matched your search
@@ -0,0 +1,72 @@
|
||||
name: Build shadPS4 (ARM64)
|
||||
|
||||
# The PS4 emulator package: the ARM64 shadPS4 core (FEXCore runs the PS4's x86-64 code) from
|
||||
# tools/shadps4/source.env, with tools/shadps4/patches so it runs stand-alone in the runtime (SDL
|
||||
# controllers, PulseAudio), packed as shadps4.AppImage. Published as a release asset the hosted
|
||||
# package catalog points at; the apk does not carry it.
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/shadps4/**'
|
||||
- '.github/workflows/build-shadps4.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to publish as (e.g. shadps4-be6bc2e-p1); empty = artifact only'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 150
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Compiler and package caches carried between runs; a run with the same pin and patches
|
||||
# recompiles only what changed.
|
||||
- uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: .ccache
|
||||
key: shadps4-ccache-${{ hashFiles('tools/shadps4/source.env') }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
shadps4-ccache-${{ hashFiles('tools/shadps4/source.env') }}-
|
||||
shadps4-ccache-
|
||||
- uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: .apt-cache
|
||||
key: shadps4-apt-${{ hashFiles('tools/shadps4/build-in-debian.sh') }}
|
||||
restore-keys: shadps4-apt-
|
||||
- name: Build in Debian trixie
|
||||
run: |
|
||||
set -euo pipefail
|
||||
df -h /
|
||||
docker run --rm -v "$PWD:/work" -w /work debian:trixie bash tools/shadps4/build-in-debian.sh
|
||||
sudo chown -R "$(id -u):$(id -g)" .
|
||||
# Saved even when the build fails or is cancelled: whatever was compiled is kept.
|
||||
- uses: actions/cache/save@v4
|
||||
if: always()
|
||||
with:
|
||||
path: .ccache
|
||||
key: shadps4-ccache-${{ hashFiles('tools/shadps4/source.env') }}-${{ github.run_id }}
|
||||
- uses: actions/cache/save@v4
|
||||
if: always()
|
||||
with:
|
||||
path: .apt-cache
|
||||
key: shadps4-apt-${{ hashFiles('tools/shadps4/build-in-debian.sh') }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: shadps4-appimage
|
||||
path: |
|
||||
shadps4.AppImage
|
||||
shadps4.AppImage.sha256
|
||||
- name: Publish
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
. tools/shadps4/source.env
|
||||
gh release create "${{ inputs.tag }}" --title "${{ inputs.tag }}" --notes "shadPS4 ARM64 (FEXCore guest CPU) from ${SHADPS4_REPO} @ ${SHADPS4_REV} with tools/shadps4/patches; installed by the app to /opt/appimages/shadps4.AppImage. sha256 $(cut -d' ' -f1 shadps4.AppImage.sha256)" shadps4.AppImage shadps4.AppImage.sha256 || gh release upload "${{ inputs.tag }}" shadps4.AppImage shadps4.AppImage.sha256 --clobber
|
||||
@@ -26,7 +26,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
PRELOAD_CACHE_VERSION: v1
|
||||
PRELOAD_CACHE_VERSION: v2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
D=app/src/main/assets/linuxfs
|
||||
mkdir -p "$D"
|
||||
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
|
||||
-pthread -std=c++17 -static-libstdc++ -static-libgcc \
|
||||
-pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \
|
||||
-o "$D/libfakeinput.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libfakeinput.so"
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
@@ -270,6 +270,12 @@ jobs:
|
||||
for sym in open openat ioctl read close poll ppoll select stat fstat access scandir; do
|
||||
grep -qx "$sym" <<<"$FAKE" || { echo "::error::libfakeinput.so does not export $sym"; exit 1; }
|
||||
done
|
||||
# The static C++ runtime stays private: every session process preloads this library,
|
||||
# and an exported personality routine takes over other programs' unwinding (shadPS4
|
||||
# aborted in pthread_exit).
|
||||
if grep -qxE '__gxx_personality_v0|__cxa_throw|_Unwind_Resume' <<<"$FAKE"; then
|
||||
echo "::error::libfakeinput.so exports the C++ runtime"; exit 1
|
||||
fi
|
||||
SESSION=$(syms "$D/libblsession.so")
|
||||
for sym in socket bind getsockname setsockopt statfs statvfs; do
|
||||
grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; }
|
||||
|
||||
@@ -576,11 +576,13 @@ class MainActivity : ComponentActivity() {
|
||||
if (catalogLoading || catalog != null) return
|
||||
catalogLoading = true
|
||||
Thread({
|
||||
val fetched = DesktopCatalog.fetch()
|
||||
val fetched = DesktopCatalog.fetch(this)
|
||||
ui.post {
|
||||
catalog = fetched
|
||||
catalogLoading = false
|
||||
refreshPackages()
|
||||
// An emulator the catalog describes gets its tile from it (Library).
|
||||
if (fetched?.any { it.frontend != null } == true) refresh()
|
||||
}
|
||||
}, "catalog-desktop").start()
|
||||
}
|
||||
@@ -1228,16 +1230,18 @@ class MainActivity : ComponentActivity() {
|
||||
else -> "Steam"
|
||||
} else null
|
||||
// The libraries, off the main thread: manifests and a folder scan.
|
||||
val known = catalog
|
||||
Thread({
|
||||
val entries = known ?: DesktopCatalog.cached(this)
|
||||
val games = if (ready) Library.steamGames(this) + com.droiddeck.launcher.frontend.AddedGames.scan(this).map { g ->
|
||||
com.droiddeck.launcher.frontend.AddedGameArt.resolve(this, g).let { art -> Library.SteamGame(g.appId.toInt(), g.name, art.portrait ?: art.header, "added", g.gameId) }
|
||||
} else emptyList()
|
||||
val emus = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null }
|
||||
val emus = Library.emulators(this, entries) { id -> DesktopCatalog.installed(this, id) != null }
|
||||
ui.post { steamGames = games; emulatorList = emus }
|
||||
// Box art for the games that have none, fetched after the list is up; the list is
|
||||
// rebuilt once if any was found.
|
||||
if (!OfflineMode.enabled(this) && CoverArt.fetchMissing(this, emus.flatMap { it.games })) {
|
||||
val refreshed = Library.emulators(this) { id -> DesktopCatalog.installed(this, id) != null }
|
||||
val refreshed = Library.emulators(this, entries) { id -> DesktopCatalog.installed(this, id) != null }
|
||||
ui.post { emulatorList = refreshed }
|
||||
}
|
||||
}, "library").start()
|
||||
|
||||
@@ -166,7 +166,7 @@ object DeviceReport {
|
||||
h("Device switch files in Download")
|
||||
for (name in listOf("droiddeck-env", "droiddeck-tu-debug", "droiddeck-driver",
|
||||
"droiddeck-osc", "droiddeck-no-pad", "droiddeck-pad-log",
|
||||
"droiddeck-no-hud", "droiddeck-wlr-renderer")) {
|
||||
"droiddeck-no-hud", "droiddeck-wlr-renderer", "droiddeck-catalog-url")) {
|
||||
val f = File(Environment.getExternalStorageDirectory(), "Download/$name")
|
||||
if (f.isFile) k(name, FileUtils.readString(f)?.trim()?.replace('\n', ' ')?.ifEmpty { "(present, empty)" } ?: "(present)")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.Downloader
|
||||
import com.droiddeck.launcher.runtime.DesktopCatalog
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.droiddeck.launcher.session.GameStorage
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
@@ -16,7 +20,8 @@ object Library {
|
||||
/** [gameId] is what steam://rungameid/ takes: the appid for a Steam title, the shortcut id for an added game. */
|
||||
class SteamGame(val appId: Int, val name: String, val art: File?, val library: String, val gameId: Long = appId.toLong())
|
||||
class Rom(val name: String, val hostPath: File, val guestPath: String, val emulatorId: String, val art: File? = null)
|
||||
class Emulator(val id: String, val name: String, val system: String, val program: String, val installed: Boolean, val games: List<Rom>) {
|
||||
/** [icon]: a catalog emulator's own icon (its row's iconUrl); the built-in ones are resources. */
|
||||
class Emulator(val id: String, val name: String, val system: String, val program: String, val installed: Boolean, val games: List<Rom>, val icon: Bitmap? = null) {
|
||||
/** The emulator's own icon, bundled (the runtime keeps them as theme SVGs the app cannot draw). */
|
||||
val iconRes: Int get() = when (id) {
|
||||
"rpcs3" -> R.drawable.emu_rpcs3; "armsx2" -> R.drawable.emu_pcsx2; "dolphin" -> R.drawable.emu_dolphin
|
||||
@@ -94,8 +99,19 @@ object Library {
|
||||
.firstOrNull { it.isFile && it.length() > 0L }
|
||||
}
|
||||
|
||||
/** [atPanel]: frames cheap enough to draw at the panel's own size (see [drawsAtPanel]). */
|
||||
private class Spec(val id: String, val name: String, val system: String, val program: String, val folders: List<String>, val exts: Set<String>, val atPanel: Boolean = false)
|
||||
/**
|
||||
* [atPanel]: frames cheap enough to draw at the panel's own size (see [drawsAtPanel]).
|
||||
* [gameFile]: a game is a folder holding this file (a PS4 dump's eboot.bin), not an image
|
||||
* file; the file is what boots, [titleSfo] names it and [artPath] is its art, both relative to
|
||||
* the folder, and a folder ending in one of [skipSuffixes] (an update beside the game) is not
|
||||
* a game. [args]: the command line, {game} standing for the game. The last four and
|
||||
* [iconUrl] come from a catalog row (see [fromCatalog]).
|
||||
*/
|
||||
private class Spec(
|
||||
val id: String, val name: String, val system: String, val program: String, val folders: List<String>, val exts: Set<String>,
|
||||
val atPanel: Boolean = false, val gameFile: String? = null, val skipSuffixes: List<String> = emptyList(),
|
||||
val titleSfo: String? = null, val artPath: String? = null, val args: List<String>? = null, val iconUrl: String? = null,
|
||||
)
|
||||
private val specs = listOf(
|
||||
Spec("rpcs3", "RPCS3", "PS3", "/opt/appimages/rpcs3.AppImage", listOf("ps3"), setOf("iso")),
|
||||
// PS2: ARMSX2, the PCSX2 fork with ARM64 recompilers. Upstream PCSX2 interprets the PS2's
|
||||
@@ -116,15 +132,58 @@ object Library {
|
||||
private fun displayTitle(name: String): String =
|
||||
name.replace(Regex("\\s*[(\\[][^)\\]]*[)\\]]"), "").trim().ifEmpty { name }
|
||||
|
||||
/** Every system folder name the specs claim, so a loose-file scan of the root skips them. */
|
||||
private val systemFolders: Set<String> by lazy { specs.flatMap { it.folders }.toSet() }
|
||||
/** The emulators the catalog describes that the app has no tile of its own for (the last read). */
|
||||
@Volatile private var catalogSpecs: List<Spec> = emptyList()
|
||||
private fun allSpecs() = specs + catalogSpecs
|
||||
|
||||
/**
|
||||
* A catalog row's "frontend" block as a tile: {"system": "PS4", "folders": ["ps4"],
|
||||
* "exts": [...] or "gameFile": "eboot.bin", "skipSuffixes": [...], "titleSfo": ..., "art": ...,
|
||||
* "args": ["-g", "{game}"], "iconUrl": ..., "program": ..., "atPanel": false}. The program
|
||||
* defaults to where the catalog puts an appimage.
|
||||
*/
|
||||
private fun fromCatalog(entries: List<DesktopCatalog.Entry>): List<Spec> {
|
||||
val builtIn = specs.map { it.id }.toSet()
|
||||
fun strings(a: org.json.JSONArray?) = a?.let { arr -> (0 until arr.length()).map { arr.optString(it) }.filter { it.isNotEmpty() } }
|
||||
return entries.mapNotNull { e ->
|
||||
val f = e.frontend ?: return@mapNotNull null
|
||||
if (e.id in builtIn) return@mapNotNull null
|
||||
runCatching {
|
||||
Spec(
|
||||
e.id, e.name, f.optString("system", e.name),
|
||||
f.optString("program", "").ifEmpty { "/opt/appimages/${e.id}.AppImage" },
|
||||
strings(f.optJSONArray("folders")).orEmpty().map { it.lowercase() },
|
||||
strings(f.optJSONArray("exts")).orEmpty().map { it.lowercase() }.toSet(),
|
||||
atPanel = f.optBoolean("atPanel", false),
|
||||
gameFile = f.optString("gameFile", "").ifEmpty { null },
|
||||
skipSuffixes = strings(f.optJSONArray("skipSuffixes")).orEmpty().map { it.uppercase() },
|
||||
titleSfo = f.optString("titleSfo", "").ifEmpty { null },
|
||||
artPath = f.optString("art", "").ifEmpty { null },
|
||||
args = strings(f.optJSONArray("args")),
|
||||
iconUrl = f.optString("iconUrl", "").takeIf { it.startsWith("https://") },
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
/** A catalog emulator's icon, kept in files/emu-icons once fetched. */
|
||||
private fun catalogIcon(context: Context, spec: Spec): Bitmap? {
|
||||
val url = spec.iconUrl ?: return null
|
||||
val file = File(context.filesDir, "emu-icons/${spec.id}.png")
|
||||
if (!file.isFile || file.length() == 0L) {
|
||||
file.parentFile?.mkdirs()
|
||||
val part = File(file.path + ".part")
|
||||
if (!Downloader.downloadFile(url, part, false, null) || !part.renameTo(file)) { part.delete(); return null }
|
||||
}
|
||||
return runCatching { BitmapFactory.decodeFile(file.path) }.getOrNull()
|
||||
}
|
||||
private val installedIds = mapOf(
|
||||
"rpcs3" to "rpcs3", "armsx2" to "armsx2", "dolphin" to "dolphin", "duckstation" to "duckstation",
|
||||
"melonds" to "melonds", "cemu" to "cemu", "ppsspp" to "emulators", "retroarch" to "emulators",
|
||||
"melonds" to "melonds", "cemu" to "cemu", "shadps4" to "shadps4", "ppsspp" to "emulators", "retroarch" to "emulators",
|
||||
)
|
||||
|
||||
/** The emulator's name for a program path from the rail ("ARMSX2"), or null. */
|
||||
fun nameForProgram(program: String?): String? = specs.firstOrNull { it.program == program }?.name
|
||||
fun nameForProgram(program: String?): String? = allSpecs().firstOrNull { it.program == program }?.name
|
||||
|
||||
/**
|
||||
* Whether a program from the rail runs at the panel's own resolution rather than the session's
|
||||
@@ -132,17 +191,24 @@ object Library {
|
||||
* nothing, and 720p scaled up to the panel blurs the sharp pixels its screen layout is set
|
||||
* up for (bannerlator-pad-defaults).
|
||||
*/
|
||||
fun drawsAtPanel(program: String?): Boolean = specs.any { it.program == program && it.atPanel }
|
||||
fun drawsAtPanel(program: String?): Boolean = allSpecs().any { it.program == program && it.atPanel }
|
||||
|
||||
/** Desktop catalog package that supplies this emulator. */
|
||||
fun packageId(emulatorId: String): String? = installedIds[emulatorId]
|
||||
fun packageId(emulatorId: String): String? = installedIds[emulatorId] ?: catalogSpecs.firstOrNull { it.id == emulatorId }?.id
|
||||
|
||||
/** Every emulator the app knows, installed or not, with the games its system folder holds. */
|
||||
fun emulators(context: Context, installedPackage: (String) -> Boolean): List<Emulator> {
|
||||
/**
|
||||
* Every emulator the app knows, installed or not, with the games its system folder holds:
|
||||
* its own, then those [catalog] describes.
|
||||
*/
|
||||
fun emulators(context: Context, catalog: List<DesktopCatalog.Entry>?, installedPackage: (String) -> Boolean): List<Emulator> {
|
||||
val romsRoot = SessionPrefs.romsDir(context).takeIf { it.isNotEmpty() }?.let(::File)?.takeIf { it.isDirectory }
|
||||
return specs.map { spec ->
|
||||
if (catalog != null) catalogSpecs = fromCatalog(catalog)
|
||||
val all = allSpecs()
|
||||
// Every system folder name the specs claim, so a loose-file scan of the root skips them.
|
||||
val systemFolders = all.flatMap { it.folders }.toSet()
|
||||
return all.map { spec ->
|
||||
val games = ArrayList<Rom>()
|
||||
if (romsRoot != null && spec.exts.isNotEmpty()) {
|
||||
if (romsRoot != null && (spec.exts.isNotEmpty() || spec.gameFile != null)) {
|
||||
// The system's folder(s), matched without regard to case, and up to three folders
|
||||
// inside them - a dump usually comes as a folder named for the game with the image
|
||||
// inside it, and people sort those into folders of their own (ps2/games/<game>/).
|
||||
@@ -164,6 +230,20 @@ object Library {
|
||||
art = File(dir, "PS3_GAME/ICON0.PNG").takeIf { it.isFile }))
|
||||
continue
|
||||
}
|
||||
// A game that is a folder (a PS4 dump: eboot.bin, sce_sys/param.sfo), booted by
|
||||
// the file in it.
|
||||
if (spec.gameFile != null) {
|
||||
val boot = File(dir, spec.gameFile)
|
||||
val upper = dir.name.uppercase()
|
||||
if (boot.isFile && spec.skipSuffixes.none { upper.endsWith(it) }) {
|
||||
val sfo = spec.titleSfo?.let { File(dir, it) }?.takeIf { it.isFile }
|
||||
val title = sfo?.let { readSfo(it)["TITLE"]?.trim()?.takeIf { t -> t.isNotEmpty() } }
|
||||
val rel = boot.relativeTo(romsRoot).path
|
||||
games.add(Rom(title ?: displayTitle(dir.name), dir, "/root/ROMs/$rel", spec.id,
|
||||
art = spec.artPath?.let { File(dir, it) }?.takeIf { it.isFile }))
|
||||
}
|
||||
continue
|
||||
}
|
||||
val files = dir.listFiles()?.sortedBy { it.name.lowercase() }.orEmpty()
|
||||
// A disc sheet (.cue, .gdi) names its track files and a playlist (.m3u) its
|
||||
// discs: the sheet is the game, what it names is part of it. A .bin beside a
|
||||
@@ -182,7 +262,8 @@ object Library {
|
||||
if (spec.id == "rpcs3") games.addAll(rpcs3Installed(context))
|
||||
// A cover found for the game before (CoverArt) where it has no art of its own.
|
||||
val withArt = games.map { g -> if (g.art != null) g else CoverArt.cached(context, g)?.let { Rom(g.name, g.hostPath, g.guestPath, g.emulatorId, it) } ?: g }
|
||||
Emulator(spec.id, spec.name, spec.system, spec.program, installedPackage(installedIds.getValue(spec.id)), withArt)
|
||||
Emulator(spec.id, spec.name, spec.system, spec.program, installedPackage(installedIds[spec.id] ?: spec.id), withArt,
|
||||
catalogIcon(context, spec))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +306,7 @@ object Library {
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
/** The string fields of a PARAM.SFO (the PSP/PS3 metadata file): a small binary table. */
|
||||
/** The string fields of a PARAM.SFO (the PSP/PS3/PS4 metadata file): a small binary table. */
|
||||
private fun readSfo(file: File): Map<String, String> {
|
||||
val out = HashMap<String, String>()
|
||||
try {
|
||||
@@ -257,7 +338,8 @@ object Library {
|
||||
}
|
||||
|
||||
/** How the emulator is told which game to boot, on its command line. */
|
||||
fun launchArgs(emulatorId: String, guestPath: String): List<String> = when (emulatorId) {
|
||||
fun launchArgs(emulatorId: String, guestPath: String): List<String> =
|
||||
catalogSpecs.firstOrNull { it.id == emulatorId }?.args?.map { it.replace("{game}", guestPath) } ?: when (emulatorId) {
|
||||
"rpcs3" -> listOf("--no-gui", guestPath)
|
||||
// Straight into the game in its controller-driven full-screen UI (first-time setup there
|
||||
// too), and gone when the game is quit from its pause menu (guide button): -batch.
|
||||
|
||||
@@ -24,10 +24,37 @@ object DesktopCatalog {
|
||||
val url: String, val sha256: String, val size: Long, val notes: String,
|
||||
/** For an appimage: the icon name and menu category of its .desktop entry. */
|
||||
val icon: String, val category: String,
|
||||
/**
|
||||
* How the front end shows an emulator the app has no tile of its own for (Library):
|
||||
* system, folders, exts or gameFile, args with {game}, iconUrl. A new emulator needs only
|
||||
* its catalog row.
|
||||
*/
|
||||
val frontend: JSONObject? = null,
|
||||
)
|
||||
|
||||
fun fetch(): List<Entry>? {
|
||||
val body = Downloader.downloadString(CATALOG_URL) ?: return null
|
||||
private fun cacheFile(context: Context) = File(context.filesDir, "desktop-catalog.json")
|
||||
|
||||
/** The catalog as last read, for the front end while offline or before the fetch is back. */
|
||||
fun cached(context: Context): List<Entry>? = FileUtils.readString(cacheFile(context))?.let(::parse)
|
||||
|
||||
/**
|
||||
* The catalog to read: Download/droiddeck-catalog-url (one https URL) points this device alone
|
||||
* at another one - a catalog branch with a package that is not live for everyone yet.
|
||||
*/
|
||||
private fun catalogUrl(): String {
|
||||
val override = File(android.os.Environment.getExternalStorageDirectory(), "Download/droiddeck-catalog-url")
|
||||
.takeIf { it.isFile }?.let { FileUtils.readString(it)?.trim()?.lineSequence()?.firstOrNull()?.trim() }
|
||||
return override?.takeIf { it.startsWith("https://") }?.also { Log.i(TAG, "catalog override: $it") } ?: CATALOG_URL
|
||||
}
|
||||
|
||||
fun fetch(context: Context? = null): List<Entry>? {
|
||||
val body = Downloader.downloadString(catalogUrl()) ?: return null
|
||||
val entries = parse(body)
|
||||
if (entries != null && context != null) FileUtils.writeString(cacheFile(context), body)
|
||||
return entries
|
||||
}
|
||||
|
||||
private fun parse(body: String): List<Entry>? {
|
||||
return try {
|
||||
val json = JSONObject(body)
|
||||
val arr = json.getJSONArray("packages")
|
||||
@@ -44,6 +71,7 @@ object DesktopCatalog {
|
||||
o.optString("kind", "tar"), file.getString("url"), file.optString("sha256", ""),
|
||||
file.optLong("size", 0L), o.optString("notes", ""),
|
||||
o.optString("icon", "applications-games"), o.optString("category", "Game"),
|
||||
o.optJSONObject("frontend"),
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -964,7 +964,7 @@ private fun Content(
|
||||
}
|
||||
Rise(4) { SectionTitle("Emulators", "${s.emulators.count { it.installed }} installed · ${s.emulators.count { !it.installed }} available") }
|
||||
Rise(5, Modifier.weight(1f).fillMaxWidth()) {
|
||||
ArtGrid(s.emulators.map { e -> Tile(e.name, if (e.installed) (if (e.id == "retroarch") null else "${e.games.size} game${if (e.games.size == 1) "" else "s"}") else "Select to install", null, "emu:${e.id}", e.iconRes, dim = !e.installed) { onSelect("emu:${e.id}") } })
|
||||
ArtGrid(s.emulators.map { e -> Tile(e.name, if (e.installed) (if (e.id == "retroarch") null else "${e.games.size} game${if (e.games.size == 1) "" else "s"}") else "Select to install", null, "emu:${e.id}", e.iconRes.takeIf { e.icon == null }, dim = !e.installed, iconBitmap = e.icon) { onSelect("emu:${e.id}") } })
|
||||
}
|
||||
}
|
||||
selected == "setup" -> SetupPanel(s, a, onOpenDeveloperOptions, onRequestWirelessAdb)
|
||||
@@ -983,7 +983,7 @@ private fun Content(
|
||||
if (e.installed) {
|
||||
Rise(3) {
|
||||
Actions {
|
||||
Image(painterResource(e.iconRes), null, modifier = Modifier.size(40.dp))
|
||||
EmulatorIcon(e)
|
||||
PrimaryButton("Open ${e.name}", enabled = s.ready && !s.busy, main = true) { a.onEmulator(e) }
|
||||
SecondaryButton("ROMs folder", onClick = a.onRoms)
|
||||
if (pkg != null) SecondaryButton(
|
||||
@@ -1001,12 +1001,12 @@ private fun Content(
|
||||
)
|
||||
}
|
||||
else Rise(5, Modifier.weight(1f).fillMaxWidth()) {
|
||||
ArtGrid(e.games.mapIndexed { index, g -> Tile(g.name, if (g.art != null) "installed" else g.hostPath.extension.uppercase().ifEmpty { "folder" }, g.art, "rom:${e.id}:$index", e.iconRes) { onSelect("rom:${e.id}:$index") } }, wide = e.games.none { it.art != null })
|
||||
ArtGrid(e.games.mapIndexed { index, g -> Tile(g.name, if (g.art != null) "installed" else g.hostPath.extension.uppercase().ifEmpty { "folder" }, g.art, "rom:${e.id}:$index", e.iconRes.takeIf { e.icon == null }, iconBitmap = e.icon) { onSelect("rom:${e.id}:$index") } }, wide = e.games.none { it.art != null })
|
||||
}
|
||||
} else {
|
||||
if (pkg != null) Rise(2) {
|
||||
Actions {
|
||||
Image(painterResource(e.iconRes), null, modifier = Modifier.size(40.dp))
|
||||
EmulatorIcon(e)
|
||||
PrimaryButton(
|
||||
if (s.packageBusyId == pkg.id) "Installing…" else "Install ${e.name}",
|
||||
enabled = s.packageBusyId == null && s.ready && !s.packageCatalogLoading && !s.sessionRunning,
|
||||
@@ -1060,7 +1060,7 @@ private fun Content(
|
||||
Text(g.guestPath, fontSize = 12.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(colors.surface).padding(horizontal = 10.dp, vertical = 6.dp))
|
||||
Spacer(Modifier.height(14.dp))
|
||||
Actions {
|
||||
Image(painterResource(e.iconRes), null, modifier = Modifier.size(40.dp))
|
||||
EmulatorIcon(e)
|
||||
PrimaryButton("Launch in ${e.name}", enabled = s.ready && !s.busy, main = true) { a.onRom(g) }
|
||||
Chip(g.hostPath.extension.uppercase().ifEmpty { "folder" }, ok = false)
|
||||
}
|
||||
@@ -1074,7 +1074,7 @@ private fun Content(
|
||||
Rise(4, Modifier.weight(1f).fillMaxWidth()) {
|
||||
ArtGrid(others.map { x ->
|
||||
val index = e.games.indexOf(x)
|
||||
Tile(x.name, if (x.art != null) "installed" else x.hostPath.extension.uppercase().ifEmpty { "folder" }, x.art, "rom:${e.id}:$index", e.iconRes) { onSelect("rom:${e.id}:$index") }
|
||||
Tile(x.name, if (x.art != null) "installed" else x.hostPath.extension.uppercase().ifEmpty { "folder" }, x.art, "rom:${e.id}:$index", e.iconRes.takeIf { e.icon == null }, iconBitmap = e.icon) { onSelect("rom:${e.id}:$index") }
|
||||
}, wide = others.none { it.art != null })
|
||||
}
|
||||
}
|
||||
@@ -1444,6 +1444,14 @@ private fun CoverImage(art: File, modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
/** An emulator's icon in its page's header: the catalog's own, else the bundled one. */
|
||||
@Composable
|
||||
private fun EmulatorIcon(e: Library.Emulator) {
|
||||
val icon = e.icon
|
||||
if (icon != null) Image(bitmap = icon.asImageBitmap(), contentDescription = null, modifier = Modifier.size(40.dp))
|
||||
else Image(painterResource(e.iconRes), null, modifier = Modifier.size(40.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Art(art: File?, iconRes: Int?, label: String, modifier: Modifier, wide: Boolean = false, iconBitmap: Bitmap? = null) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
|
||||
@@ -7,6 +7,42 @@ the timeline, then lessons and backlog. Companion to the README (what the app *d
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-26 - `feat/ps4-shadps4` (PARKED): PS4 through shadPS4, emulators from the catalog
|
||||
|
||||
State: **branch `feat/ps4-shadps4` = main `c950d31` + 5 commits** (head `9daf41e`, not a PR yet). Parked by
|
||||
the owner after the first game test: the DroidDeck side works; the emulator core aborts on this device.
|
||||
|
||||
- **shadPS4 package** (`tools/shadps4`, `.github/workflows/build-shadps4.yml`): the ARM64 shadPS4 core
|
||||
(zenithblue-oss/shadps4-arm64 `be6bc2e9`, from the Bachata S4 project, GPL-2.0; FEXCore runs the PS4's
|
||||
x86-64 code) cross-built in Debian trixie with a patch so it runs stand-alone: SDL controllers and SDL audio
|
||||
(PulseAudio) when there is no Bachata control socket. Packed as `shadps4.AppImage` (AppRun: no arguments =
|
||||
Big Picture game list; the first time, `ROMs/ps4` is added to its game folders). Published as a release
|
||||
asset in winlator-contents `steamdeck-desktop-r1` (the org repo is private). Needs only the runtime's own
|
||||
libraries (libc/libm/libstdc++/libgcc_s/libudev/libuuid).
|
||||
- **Emulators described by the catalog**: a `desktop.json` row with a `"frontend"` block (system, folders,
|
||||
file types or a game-folder file, title/art paths, args with `{game}`, icon URL) becomes a front-end tile
|
||||
with no app change; the last catalog read is kept for offline use. shadPS4 is the first: a game is a
|
||||
dumped folder with `eboot.bin` (title from `sce_sys/param.sfo`, art `sce_sys/icon0.png`, `-UPDATE` /
|
||||
`-patch` folders left out), launched `-f true -g <eboot>`.
|
||||
- **Per-device catalog override**: `Download/droiddeck-catalog-url` (one https URL) points one device at
|
||||
another catalog (listed in the device report). The shadPS4 row lives on winlator-contents branch
|
||||
`feat/shadps4`, not main; the test device reads it through the override.
|
||||
- **Device test** (Pocket FIT, repacked build `9daf41e`): tile appears ("Select to install"), installs,
|
||||
lists God of War III Remastered (CUSA01715) with its icon and title, launches full screen; controller
|
||||
found, Turnip Adreno 750 Vulkan up, ~55 frames presented - then the game aborts (SIGABRT, exit 134;
|
||||
the AppImage runtime reports 127, the session 0) right after the game starts a thread. Same with the 1.01
|
||||
update's eboot. **Bachata S4 0.2.3's own app fails the same way on this device**, without proot:
|
||||
`BACHATA_FEX_MAPPING_FAIL reason=invalid_proc_maps` / `FEX guest callback pthread start failed at stage 1:
|
||||
14` - an emulator-core bug at FEX guest-thread start, not DroidDeck's.
|
||||
- Games come from PKGs via the owner's separate app, PS4 PKG Extractor (private repo); GoW3 extracted and
|
||||
verified (12,022 files, 54.25 GB).
|
||||
- **Open when resumed**: prefer `<ID>-UPDATE/eboot.bin` when an update exists (shadPS4 applies an update
|
||||
only when booted from it); tile name "shadPS4" (the row's name is long); try other games; report the
|
||||
FEX thread-start failure upstream with the Bachata diagnostic; then the DroidDeck PR, and the catalog row
|
||||
last (it goes live to every install).
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-25 - main `8cdedbe`: melonDS out of the box, touch in Big Picture, log privacy, Decky
|
||||
|
||||
State: **main = `8cdedbe`** (PR #22 merge). Device-tested on the AYANEO Pocket FIT (repacked test builds)
|
||||
|
||||
@@ -100,7 +100,7 @@ docker run --rm --platform linux/amd64 \
|
||||
d=app/src/main/assets/linuxfs
|
||||
mkdir -p "$d"
|
||||
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
|
||||
-pthread -std=c++17 -static-libstdc++ -static-libgcc \
|
||||
-pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \
|
||||
-o "$d/libfakeinput.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$d/libfakeinput.so"
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
|
||||
Executable
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs INSIDE a debian:trixie container on an x86-64 runner. Cross-builds the ARM64 shadPS4 core
|
||||
# (tools/shadps4/source.env) with its own scripts - FEXCore first, then shadPS4 linked against it -
|
||||
# after applying ./patches, and packs the binary as shadps4.AppImage for /opt/appimages.
|
||||
# Built against trixie's glibc 2.41; the runtime's Arch rootfs is newer, so its own X11, Vulkan
|
||||
# loader, udev and C++ libraries are used and nothing is bundled.
|
||||
set -euxo pipefail
|
||||
WORK=/work
|
||||
cd "$WORK"
|
||||
. tools/shadps4/source.env
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
# Downloaded packages are kept in the workspace (.apt-cache), which the workflow caches between
|
||||
# runs; the image's docker-clean hook would otherwise delete them after every install.
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean
|
||||
mkdir -p "$WORK/.apt-cache/archives/partial"
|
||||
cat > /etc/apt/apt.conf.d/99droiddeck-cache <<'APT'
|
||||
Dir::Cache::archives "/work/.apt-cache/archives";
|
||||
Binary::apt::APT::Keep-Downloaded-Packages "true";
|
||||
APT
|
||||
dpkg --add-architecture arm64
|
||||
apt-get update
|
||||
# The Bachata runtime's cross set, plus PulseAudio and ALSA headers so SDL3 builds its audio
|
||||
# drivers (it loads them at run time; the runtime plays through PulseAudio), and the Wayland, EGL
|
||||
# and xkbcommon headers with the host's wayland-scanner so it builds its Wayland video driver.
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates git curl xz-utils zstd file pkg-config dpkg-dev squashfs-tools ccache \
|
||||
cmake ninja-build clang llvm lld gcc g++ make \
|
||||
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu \
|
||||
python3 nodejs \
|
||||
libvulkan-dev libc6-dev \
|
||||
libx11-dev:arm64 libxext-dev:arm64 libudev-dev:arm64 uuid-dev:arm64 \
|
||||
libpulse-dev:arm64 libasound2-dev:arm64 \
|
||||
libwayland-dev:arm64 libegl-dev:arm64 libxkbcommon-dev:arm64 libwayland-bin \
|
||||
libc6:arm64 libgcc-s1:arm64 libstdc++6:arm64 libvulkan1:arm64 libudev1:arm64 libuuid1:arm64 \
|
||||
libx11-6:arm64 libxext6:arm64
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
SRC=$WORK/build/shadps4-arm64-src
|
||||
rm -rf "$SRC"
|
||||
git init -q "$SRC"
|
||||
git -C "$SRC" remote add origin "$SHADPS4_REPO"
|
||||
git -C "$SRC" fetch -q --depth 1 origin "$SHADPS4_REV"
|
||||
git -C "$SRC" checkout -q --detach FETCH_HEAD
|
||||
test "$(git -C "$SRC" rev-parse HEAD)" = "$SHADPS4_REV"
|
||||
git -C "$SRC" submodule update --init --recursive --depth 1 --jobs 8
|
||||
for p in $(ls tools/shadps4/patches/*.patch | sort); do
|
||||
echo "applying $(basename "$p")"
|
||||
git -C "$SRC" apply --whitespace=nowarn "$WORK/$p"
|
||||
done
|
||||
|
||||
# pkg-config answers for the arm64 target, not the x86-64 host: SDL3 finds PulseAudio (libpulse)
|
||||
# only through it, and without it builds ALSA alone.
|
||||
export PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig
|
||||
pkg-config --modversion libpulse
|
||||
# FEXCore compiles through ccache on its own once it is installed, and patches/0005 hands
|
||||
# shadPS4's configure the same launcher; the cache lives in the workspace (.ccache) and the
|
||||
# workflow keeps it between runs.
|
||||
export CCACHE_DIR=$WORK/.ccache CCACHE_BASEDIR=$WORK CCACHE_NOHASHDIR=1 \
|
||||
CCACHE_COMPILERCHECK=content CCACHE_MAXSIZE=4G
|
||||
ccache -z
|
||||
bash "$SRC/runtime/scripts/build-shadps4-arm64.sh"
|
||||
ccache -s
|
||||
BIN=$SRC/runtime/build/shadps4-arm64-stage/bin/shadps4-arm64
|
||||
test -x "$BIN"
|
||||
cat "$SRC/runtime/build/shadps4-arm64-stage/needed.txt"
|
||||
# SDL3 found the audio drivers the runtime needs.
|
||||
grep -E 'SDL_AUDIO_DRIVER_(PULSEAUDIO|ALSA)' "$SRC"/runtime/build/shadps4-arm64/externals/sdl3/include-config-release/build_config/SDL_build_config.h
|
||||
grep -q '#define SDL_AUDIO_DRIVER_PULSEAUDIO 1' "$SRC"/runtime/build/shadps4-arm64/externals/sdl3/include-config-release/build_config/SDL_build_config.h
|
||||
# ... and both video drivers: X11 under gamescope, Wayland on the desktop.
|
||||
grep -q '#define SDL_VIDEO_DRIVER_WAYLAND 1' "$SRC"/runtime/build/shadps4-arm64/externals/sdl3/include-config-release/build_config/SDL_build_config.h
|
||||
grep -q '#define SDL_VIDEO_DRIVER_X11 1' "$SRC"/runtime/build/shadps4-arm64/externals/sdl3/include-config-release/build_config/SDL_build_config.h
|
||||
# The patch is in.
|
||||
grep -aq 'BACHATA_MANAGED' "$BIN"
|
||||
|
||||
APPDIR=$WORK/build/shadPS4.AppDir
|
||||
rm -rf "$APPDIR"
|
||||
install -Dm755 "$BIN" "$APPDIR/usr/bin/shadps4"
|
||||
cat > "$APPDIR/AppRun" <<'RUN'
|
||||
#!/bin/sh
|
||||
# shadPS4 is a command-line emulator; opened from a menu with nothing to run, it shows its own
|
||||
# Big Picture game list instead of a usage box.
|
||||
# The first time, the ROMs folder's ps4 folder (any case) is added to its game folders so the
|
||||
# list is not empty; a folder the player removes later stays removed.
|
||||
# Under gamescope (a game session) it stays on gamescope's X server; on the desktop it draws
|
||||
# through the compositor's Wayland directly. An SDL_VIDEODRIVER already set wins.
|
||||
HERE=$(dirname "$(readlink -f "$0")")
|
||||
if [ -z "$SDL_VIDEODRIVER" ]; then
|
||||
if [ -n "$GAMESCOPE_WAYLAND_DISPLAY" ]; then
|
||||
export SDL_VIDEODRIVER=x11
|
||||
elif [ -n "$WAYLAND_DISPLAY" ]; then
|
||||
export SDL_VIDEODRIVER=wayland
|
||||
fi
|
||||
fi
|
||||
if [ $# -eq 0 ]; then
|
||||
MARK="${XDG_DATA_HOME:-$HOME/.local/share}/shadPS4/.droiddeck-games-folder"
|
||||
if [ ! -e "$MARK" ]; then
|
||||
for d in /root/ROMs/*; do
|
||||
case "$(basename "$d" | tr 'A-Z' 'a-z')" in
|
||||
ps4) [ -d "$d" ] && "$HERE/usr/bin/shadps4" --add-game-folder "$d" && mkdir -p "$(dirname "$MARK")" && : > "$MARK" ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
set -- --big-picture
|
||||
fi
|
||||
exec "$HERE/usr/bin/shadps4" "$@"
|
||||
RUN
|
||||
chmod 755 "$APPDIR/AppRun"
|
||||
cat > "$APPDIR/shadps4.desktop" <<'DESK'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=shadPS4
|
||||
Exec=shadps4
|
||||
Icon=shadps4
|
||||
Categories=Game;Emulator;
|
||||
DESK
|
||||
cp "$SRC/src/resources/shadps4.png" "$APPDIR/shadps4.png"
|
||||
ln -sf shadps4.png "$APPDIR/.DirIcon"
|
||||
|
||||
curl -fsSL -o runtime-aarch64 "$APPIMAGE_RUNTIME_URL"
|
||||
file runtime-aarch64 | grep -q 'ARM aarch64'
|
||||
mksquashfs "$APPDIR" shadps4.squashfs -root-owned -noappend -comp zstd -Xcompression-level 19
|
||||
cat runtime-aarch64 shadps4.squashfs > shadps4.AppImage
|
||||
chmod 755 shadps4.AppImage
|
||||
rm -f shadps4.squashfs runtime-aarch64
|
||||
sha256sum shadps4.AppImage | tee shadps4.AppImage.sha256
|
||||
ls -l shadps4.AppImage
|
||||
@@ -0,0 +1,111 @@
|
||||
diff --git a/src/core/libraries/audio/audioout.cpp b/src/core/libraries/audio/audioout.cpp
|
||||
index 2e28f7d..91ec84f 100644
|
||||
--- a/src/core/libraries/audio/audioout.cpp
|
||||
+++ b/src/core/libraries/audio/audioout.cpp
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
+#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -207,14 +208,15 @@ s32 PS4_SYSV_ABI sceAudioOutInit() {
|
||||
}
|
||||
|
||||
#ifdef ENABLE_BACHATA_RUNTIME
|
||||
- audio = std::make_unique<BachataAudioOut>();
|
||||
-#else
|
||||
+ if (std::getenv("BACHATA_ALSA_SOCKET") != nullptr) {
|
||||
+ audio = std::make_unique<BachataAudioOut>();
|
||||
+ } else
|
||||
+#endif
|
||||
if (EmulatorSettings.GetAudioBackend() == AudioBackend::OpenAL) {
|
||||
audio = std::make_unique<OpenALAudioOut>();
|
||||
} else {
|
||||
audio = std::make_unique<SDLAudioOut>();
|
||||
}
|
||||
-#endif
|
||||
|
||||
LOG_INFO(Lib_AudioOut, "Audio system initialized");
|
||||
return ORBIS_OK;
|
||||
diff --git a/src/main.cpp b/src/main.cpp
|
||||
index 13733ed..d28319e 100644
|
||||
--- a/src/main.cpp
|
||||
+++ b/src/main.cpp
|
||||
@@ -298,6 +298,8 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
runtime_client = std::move(*connected);
|
||||
+ // Stand-alone runs (no socket) keep SDL controllers and SDL audio.
|
||||
+ setenv("BACHATA_MANAGED", "1", 1);
|
||||
if (!runtime_client.SendStarting()) {
|
||||
std::cerr << "Failed to send Bachata Starting event\n";
|
||||
return 1;
|
||||
diff --git a/src/sdl_window.cpp b/src/sdl_window.cpp
|
||||
index ffa42d3..0785fc3 100644
|
||||
--- a/src/sdl_window.cpp
|
||||
+++ b/src/sdl_window.cpp
|
||||
@@ -27,6 +27,8 @@
|
||||
#include "input/input_handler.h"
|
||||
#include "input/input_mouse.h"
|
||||
#include "sdl_window.h"
|
||||
+
|
||||
+#include <cstdlib>
|
||||
#include "video_core/renderdoc.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
@@ -39,6 +41,14 @@ CMRC_DECLARE(res);
|
||||
|
||||
namespace Frontend {
|
||||
|
||||
+#ifdef ENABLE_BACHATA_RUNTIME
|
||||
+// The Android runtime delivers controllers over its socket; a stand-alone run uses SDL like
|
||||
+// the desktop build.
|
||||
+static bool BachataManaged() {
|
||||
+ return std::getenv("BACHATA_MANAGED") != nullptr;
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
using namespace Libraries::Pad;
|
||||
|
||||
static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) {
|
||||
@@ -153,9 +163,10 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controller
|
||||
SDL_SetWindowFullscreen(window, EmulatorSettings.IsFullScreen());
|
||||
SDL_SyncWindow(window);
|
||||
|
||||
-#ifndef ENABLE_BACHATA_RUNTIME
|
||||
- SDL_InitSubSystem(SDL_INIT_GAMEPAD);
|
||||
+#ifdef ENABLE_BACHATA_RUNTIME
|
||||
+ if (!BachataManaged())
|
||||
#endif
|
||||
+ SDL_InitSubSystem(SDL_INIT_GAMEPAD);
|
||||
|
||||
#if defined(SDL_PLATFORM_WIN32)
|
||||
window_info.type = WindowSystemType::Windows;
|
||||
@@ -183,9 +194,10 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controller
|
||||
// input handler init-s
|
||||
Input::ControllerOutput::LinkJoystickAxes();
|
||||
Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
|
||||
-#ifndef ENABLE_BACHATA_RUNTIME
|
||||
- controllers.TryOpenSDLControllers();
|
||||
+#ifdef ENABLE_BACHATA_RUNTIME
|
||||
+ if (!BachataManaged())
|
||||
#endif
|
||||
+ controllers.TryOpenSDLControllers();
|
||||
|
||||
if (EmulatorSettings.IsBackgroundControllerInput()) {
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
@@ -266,9 +278,10 @@ void WindowSDL::WaitEvent() {
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_ADDED:
|
||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||
-#ifndef ENABLE_BACHATA_RUNTIME
|
||||
- controllers.TryOpenSDLControllers();
|
||||
+#ifdef ENABLE_BACHATA_RUNTIME
|
||||
+ if (!BachataManaged())
|
||||
#endif
|
||||
+ controllers.TryOpenSDLControllers();
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||
@@ -0,0 +1,26 @@
|
||||
diff --git a/runtime/scripts/build-shadps4-arm64.sh b/runtime/scripts/build-shadps4-arm64.sh
|
||||
index 8739b7a..49c35fa 100644
|
||||
--- a/runtime/scripts/build-shadps4-arm64.sh
|
||||
+++ b/runtime/scripts/build-shadps4-arm64.sh
|
||||
@@ -98,7 +98,8 @@ cmake -S "$project_root" -B "$build_dir" -G Ninja \
|
||||
-DSDL_X11_XSHAPE=OFF \
|
||||
-DSDL_X11_XSYNC=OFF \
|
||||
-DSDL_X11_XTEST=OFF \
|
||||
- -DSDL_WAYLAND=OFF
|
||||
+ -DSDL_WAYLAND=ON \
|
||||
+ -DSDL_WAYLAND_LIBDECOR=OFF
|
||||
|
||||
sdl_config="$build_dir/externals/sdl3/include-config-release/build_config/SDL_build_config.h"
|
||||
if ! grep -Fq '#define SDL_VIDEO_DRIVER_X11_DYNAMIC_XEXT "libXext.so.6"' "$sdl_config"; then
|
||||
@@ -106,6 +107,11 @@ if ! grep -Fq '#define SDL_VIDEO_DRIVER_X11_DYNAMIC_XEXT "libXext.so.6"' "$sdl_c
|
||||
exit 1
|
||||
fi
|
||||
|
||||
+if ! grep -Fq '#define SDL_VIDEO_DRIVER_WAYLAND_DYNAMIC "libwayland-client.so.0"' "$sdl_config"; then
|
||||
+ echo "ARM64 SDL3 requires dynamic Wayland support; install libwayland-dev:arm64, libegl-dev:arm64 and libxkbcommon-dev:arm64" >&2
|
||||
+ exit 1
|
||||
+fi
|
||||
+
|
||||
cmake --build "$build_dir" --target shadps4
|
||||
|
||||
test -f "$binary"
|
||||
@@ -0,0 +1,35 @@
|
||||
diff --git a/src/core/memory.cpp b/src/core/memory.cpp
|
||||
index 7e17cdc..26cac97 100644
|
||||
--- a/src/core/memory.cpp
|
||||
+++ b/src/core/memory.cpp
|
||||
@@ -550,6 +550,30 @@ s32 MemoryManager::MapMemory(void** out_addr, VAddr virtual_addr, u64 size, Memo
|
||||
}
|
||||
}
|
||||
|
||||
+ // A 39-bit Android address space ends far below the top of the PS4's user area, where some
|
||||
+ // engines ask for fixed mappings (EA's 'EAOSGlobal' at 0xfbffffc000). Place such a mapping at
|
||||
+ // the top of the range this device has instead of asserting; the caller gets the new address.
|
||||
+ if (True(flags & MemoryMapFlags::Fixed) && !IsValidMapping(virtual_addr, size)) {
|
||||
+ const VAddr requested = virtual_addr;
|
||||
+ const u64 align = alignment > 16_KB ? alignment : 16_KB;
|
||||
+ const VAddr top = impl.UserVirtualBase() + impl.UserVirtualSize();
|
||||
+ VAddr candidate = Common::AlignDown(top - size, align);
|
||||
+ const auto it = FindVMA(candidate);
|
||||
+ if (it == vma_map.end() || !it->second.IsFree() || !it->second.Contains(candidate, size)) {
|
||||
+ candidate = SearchFree(DEFAULT_MAPPING_BASE, size, static_cast<u32>(align));
|
||||
+ }
|
||||
+ if (candidate == static_cast<VAddr>(-1)) {
|
||||
+ LOG_ERROR(Kernel_Vmm, "No room to relocate fixed mapping {:#x} ({:#x} bytes)",
|
||||
+ requested, size);
|
||||
+ return ORBIS_KERNEL_ERROR_ENOMEM;
|
||||
+ }
|
||||
+ LOG_WARNING(Kernel_Vmm,
|
||||
+ "Fixed mapping {:#x} ({:#x} bytes, '{}') is outside this device's address "
|
||||
+ "space; relocated to {:#x}",
|
||||
+ requested, size, name, candidate);
|
||||
+ virtual_addr = candidate;
|
||||
+ }
|
||||
+
|
||||
if (True(flags & MemoryMapFlags::Fixed) && True(flags & MemoryMapFlags::NoOverwrite)) {
|
||||
// Perform necessary error checking for Fixed & NoOverwrite case
|
||||
ASSERT_MSG(IsValidMapping(virtual_addr, size), "Attempted to access invalid address {:#x}",
|
||||
@@ -0,0 +1,16 @@
|
||||
diff --git a/src/core/fex/fex_guest_engine.cpp b/src/core/fex/fex_guest_engine.cpp
|
||||
index 2271f97..71ddc6d 100644
|
||||
--- a/src/core/fex/fex_guest_engine.cpp
|
||||
+++ b/src/core/fex/fex_guest_engine.cpp
|
||||
@@ -182,7 +182,10 @@ EngineResult<bool> ValidateHostMapping(const GuestExecutionRange& range) {
|
||||
static_cast<unsigned long>(begin), static_cast<unsigned long>(end), permissions);
|
||||
return Failure(EngineStage::Mapping, EACCES);
|
||||
}
|
||||
- if (range.Writable && (permissions[1] != 'w' || permissions[2] == 'x')) {
|
||||
+ // A guard hole the guest protected itself (PROT_NONE, "---p") inside a writable range is
|
||||
+ // the game's own doing, like a stack guard page; FEX never writes there on its behalf.
|
||||
+ const bool guardHole = permissions[0] == '-' && permissions[1] == '-' && permissions[2] == '-';
|
||||
+ if (range.Writable && !guardHole && (permissions[1] != 'w' || permissions[2] == 'x')) {
|
||||
std::fprintf(stderr,
|
||||
"BACHATA_FEX_MAPPING_FAIL reason=host_write_permission error=%d begin=%#lx "
|
||||
"size=%#lx executable=%d writable=%d host_begin=%#lx host_end=%#lx "
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/runtime/scripts/build-shadps4-arm64.sh b/runtime/scripts/build-shadps4-arm64.sh
|
||||
index 49c35fa..2bdcba0 100644
|
||||
--- a/runtime/scripts/build-shadps4-arm64.sh
|
||||
+++ b/runtime/scripts/build-shadps4-arm64.sh
|
||||
@@ -74,6 +74,8 @@ cmake -S "$project_root" -B "$build_dir" -G Ninja \
|
||||
-DXEXT_LIB:FILEPATH="$arm64_xext_lib" \
|
||||
-DIMGUI_FONT_EMBED_EXECUTABLE="$host_font_embed" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
+ -DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
+ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_SCAN_FOR_MODULES=OFF \
|
||||
-DCMAKE_AR="$llvm_ar" \
|
||||
-DCMAKE_RANLIB="$llvm_ranlib" \
|
||||
@@ -0,0 +1,6 @@
|
||||
# The ARM64 shadPS4 core: upstream shadPS4 with FEXCore running the PS4's x86-64 code, published
|
||||
# by the Bachata S4 project (GPL-2.0). Pinned; tools/shadps4/patches apply on top.
|
||||
SHADPS4_REPO=https://github.com/zenithblue-oss/shadps4-arm64.git
|
||||
SHADPS4_REV=be6bc2e9c60799e071dd2fafa6216e8d80ec619c
|
||||
# AppImage type2 runtime for aarch64 (squashfs with zstd).
|
||||
APPIMAGE_RUNTIME_URL=https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-aarch64
|
||||
Reference in new issue
Block a user