diff --git a/src/MeloNX-Skip/melonx-native/Android/app/src/main/kotlin/Main.kt b/src/MeloNX-Skip/melonx-native/Android/app/src/main/kotlin/Main.kt index 896008bea..ac004340d 100644 --- a/src/MeloNX-Skip/melonx-native/Android/app/src/main/kotlin/Main.kt +++ b/src/MeloNX-Skip/melonx-native/Android/app/src/main/kotlin/Main.kt @@ -7,6 +7,8 @@ import skip.ui.* import android.Manifest import android.app.Application +import android.hardware.Sensor +import android.util.Log import androidx.activity.enableEdgeToEdge import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity @@ -17,6 +19,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -25,6 +28,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.app.ActivityCompat +import melo.nxmodel.Ryujinx import org.libsdl.app.SDLActivity internal val logger: SkipLogger = SkipLogger(subsystem = "melonx.module", category = "melonx") @@ -56,9 +60,12 @@ open class MainActivity: SDLActivity { enableEdgeToEdge() setContent { + val gameIsRunning = remember { GameState.shared._isGameRunning.projectedValue } + val startGameConfig = remember { GameState.shared._startGameConfig.projectedValue } + Box { SDLComposeSurface() - Box(Modifier.graphicsLayer(alpha = 0.5f)) { + if (gameIsRunning?.value != true) { val saveableStateHolder = rememberSaveableStateHolder() saveableStateHolder.SaveableStateProvider(true) { PresentationRootView(ComposeContext()) @@ -66,6 +73,11 @@ open class MainActivity: SDLActivity { } } } + + if (startGameConfig?.value != null) { + runSimulator(GameState.shared.startGameConfig!!) +// GameState.shared.startGameConfig = null + } } // Example of requesting permissions on startup. @@ -139,6 +151,31 @@ open class MainActivity: SDLActivity { } ) } + + fun runSimulator(config: Ryujinx.Configuration) { + class RyujinxMain : Runnable { + override fun run() { + try { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DISPLAY) + } catch (e: Exception) { + Log.v("SDL", "modify thread properties failed $e") + } + + try { + Ryujinx.shared.start(config) + } catch (e: Exception) { + Log.v("Ryujinx", "Emulation failed to start $e") + } + } + } + + // This is the entry point to the C app. + // Start up the C app thread and enable sensor input for the first time + // FIXME: Why aren't we enabling sensor input at start? + mSDLThread = Thread(RyujinxMain(), "SDLThread") + mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true) + mSDLThread.start() + } } @Composable diff --git a/src/MeloNX-Skip/melonx-native/Sources/LibRyujinx/include/demo.h b/src/MeloNX-Skip/melonx-native/Sources/LibRyujinx/include/demo.h index e0c241d86..d5d8cc38b 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/LibRyujinx/include/demo.h +++ b/src/MeloNX-Skip/melonx-native/Sources/LibRyujinx/include/demo.h @@ -24,6 +24,12 @@ extern "C" { SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR \ ) +typedef struct { + unsigned char data[16]; +} SDL_GUID; + +typedef SDL_GUID SDL_JoystickGUID; + struct GameInfo { long FileSize; char TitleName[512]; diff --git a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Ryujinx.swift b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Ryujinx.swift index aef639c4e..17b1f5e14 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Ryujinx.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/MeloNXModel/Ryujinx.swift @@ -19,6 +19,7 @@ import Darwin @Observable public class Ryujinx { public var games: [Game] = [] public var firmwareversion = "0" + public var controllers: [Controller] = [] public static let shared = Ryujinx() private init() {} @@ -27,11 +28,18 @@ import Darwin private var basePath: URL! } + +public struct Controller: Identifiable, Hashable { + public var id: String + public var name: String +} + public extension Ryujinx { func initialize(basePath: URL) { self.basePath = basePath SDLLib.shared.initSDL() RyujinxLib.shared.initialize(basePath: basePath.path()) + refreshConnectedControllers() loadGames() } @@ -80,7 +88,7 @@ public extension Ryujinx { games.append(game) } catch { - print(error) + logger.error("\(error)") } } @@ -129,6 +137,32 @@ public extension Ryujinx { func getCurrentFps() -> Int { RyujinxLib.shared.getCurrentFps() } + + func refreshConnectedControllers() { + var controllers: [Controller] = [] + + let numJoysticks = SDLLib.shared.SDL_NumJoysticks() + + for i in 0.. String? { + let guid = SDLLib.shared.SDL_JoystickGetDeviceGUID(joystickIndex) + + if guid.data.0 == 0 && guid.data.1 == 0 && guid.data.2 == 0 && guid.data.3 == 0 { + return nil + } + + let reorderedGUID: [UInt8] = [ + guid.data.3, guid.data.2, guid.data.1, guid.data.0, + guid.data.5, guid.data.4, + guid.data.7, guid.data.6, + guid.data.8, guid.data.9, + guid.data.10, guid.data.11, guid.data.12, guid.data.13, guid.data.14, guid.data.15 + ] + + let guidString = reorderedGUID.map { String(format: "%02X", $0) }.joined().lowercased() + + func substring(_ str: String, _ start: Int, _ end: Int) -> String { + let startIdx = str.index(str.startIndex, offsetBy: start) + let endIdx = str.index(str.startIndex, offsetBy: end) + return String(str[startIdx.. Int { + let sym = dlsym(handle, "SDL_NumJoysticks") + + typealias SDL_NumJoysticks = @convention(c) () -> (Int32) + let f = unsafeBitCast(sym, to: SDL_NumJoysticks.self) + return Int(f()) + } + + func SDL_GameControllerOpen(_ joystick_index: Int) -> OpaquePointer! { + let sym = dlsym(handle, "SDL_GameControllerOpen") + + typealias SDL_GameControllerOpen = @convention(c) (Int32) -> (OpaquePointer?) + let f = unsafeBitCast(sym, to: SDL_GameControllerOpen.self) + return f(Int32(joystick_index)) + } + + func SDL_GameControllerName(_ gamecontroller: OpaquePointer!) -> UnsafePointer! { + let sym = dlsym(handle, "SDL_GameControllerName") + + typealias SDL_GameControllerName = @convention(c) (OpaquePointer?) -> (UnsafePointer?) + let f = unsafeBitCast(sym, to: SDL_GameControllerName.self) + return f(gamecontroller) + } + + func SDL_GameControllerClose(_ gamecontroller: OpaquePointer!) { + let sym = dlsym(handle, "SDL_GameControllerClose") + + typealias SDL_GameControllerClose = @convention(c) (OpaquePointer?) -> () + let f = unsafeBitCast(sym, to: SDL_GameControllerClose.self) + f(gamecontroller) + } + + func SDL_JoystickGetDeviceGUID(_ device_index: Int) -> SDL_JoystickGUID { + let sym = dlsym(handle, "SDL_JoystickGetDeviceGUID") + + typealias SDL_JoystickGetDeviceGUID = @convention(c) (Int32) -> (SDL_JoystickGUID) + let f = unsafeBitCast(sym, to: SDL_JoystickGetDeviceGUID.self) + return f(Int32(device_index)) + } } diff --git a/src/MeloNX-Skip/melonx-native/Sources/melonx/ContentView.swift b/src/MeloNX-Skip/melonx-native/Sources/melonx/ContentView.swift index 09b6973d8..5d7638601 100644 --- a/src/MeloNX-Skip/melonx-native/Sources/melonx/ContentView.swift +++ b/src/MeloNX-Skip/melonx-native/Sources/melonx/ContentView.swift @@ -6,6 +6,12 @@ enum ContentTab: String, Hashable { case games, home, settings } +@Observable public class GameState { + public static var shared = GameState() + public var isGameRunning: Bool = false + public var startGameConfig: Ryujinx.Configuration? +} + struct ContentView: View { @AppStorage("tab") var tab = ContentTab.games @State var viewModel = ViewModel() @@ -72,16 +78,19 @@ private extension ContentView { .onAppear { setupEmulation() - Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in - if Ryujinx.shared.getCurrentFps() != 0 { - withAnimation { - isLoading = false - } + Task { + try await Task.sleep(nanoseconds: 5_000_000_000) + Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in + if Ryujinx.shared.getCurrentFps() != 0 { + withAnimation { + isLoading = false + } -// isAnimating = false - timer.invalidate() + GameState.shared.isGameRunning = true + // isAnimating = false + timer.invalidate() + } } - logger.info("FPS: \(Ryujinx.shared.getCurrentFps())") } } @@ -91,16 +100,15 @@ private extension ContentView { // patchMakeKeyAndVisible() // isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil) - DispatchQueue.main.async { - start() - } + start() } func start() { guard let game else { return } config.gamepath = game.fileURL.path -// config.inputids = Array(Set(currentControllers.map(\.id))) + Ryujinx.shared.refreshConnectedControllers() + config.inputids = Ryujinx.shared.controllers.map(\.id) // // if mVKPreFillBuffer { // let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2") @@ -116,10 +124,11 @@ private extension ContentView { config.inputids.append("0") } - do { - try Ryujinx.shared.start(with: config) - } catch { - logger.error("Error: \(error.localizedDescription)") - } +// do { + GameState.shared.startGameConfig = config +// try Ryujinx.shared.start(with: config) +// } catch { +// logger.error("Error: \(error.localizedDescription)") +// } } }