forked from MeloNX/MeloNX
368 lines
14 KiB
Swift
368 lines
14 KiB
Swift
//
|
|
// ContentView.swift
|
|
// MeloNX
|
|
//
|
|
// Created by Stossy11 on 3/11/2024.
|
|
//
|
|
|
|
import SwiftUI
|
|
// import SDL2
|
|
import GameController
|
|
import Darwin
|
|
import UIKit
|
|
import MetalKit
|
|
// import SDL
|
|
|
|
struct MoltenVKSettings: Codable, Hashable {
|
|
let string: String
|
|
var value: String
|
|
}
|
|
|
|
struct ContentView: View {
|
|
// Games
|
|
@State private var game: Game?
|
|
|
|
// Controllers
|
|
@State private var controllersList: [Controller] = []
|
|
@State private var currentControllers: [Controller] = []
|
|
@State var onscreencontroller: Controller = Controller(id: "", name: "")
|
|
@State private var isVirtualControllerActive: Bool = false
|
|
@AppStorage("isVirtualController") var isVCA: Bool = true
|
|
|
|
// Settings and Configuration
|
|
@State private var config: Ryujinx.Configuration
|
|
@State var settings: [MoltenVKSettings]
|
|
@AppStorage("useTrollStore") var useTrollStore: Bool = false
|
|
|
|
// JIT
|
|
@AppStorage("JIT") var isJITEnabled: Bool = false
|
|
|
|
// Other Configuration
|
|
@State var isMK8: Bool = false
|
|
@AppStorage("quit") var quit: Bool = false
|
|
@State var quits: Bool = false
|
|
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
|
|
|
|
// Loading Animation
|
|
@State private var clumpOffset: CGFloat = -100
|
|
private let clumpWidth: CGFloat = 100
|
|
private let animationDuration: Double = 1.0
|
|
@State private var isAnimating = false
|
|
@State var isLoading = true
|
|
|
|
// MARK: - Initialization
|
|
init() {
|
|
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
|
_config = State(initialValue: defaultConfig)
|
|
|
|
let defaultSettings: [MoltenVKSettings] = [
|
|
// MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "1"),
|
|
// MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2"),
|
|
// Metal Private API isn't needed and causes more stutters
|
|
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
|
|
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
|
|
MoltenVKSettings(string: "MVK_DEBUG", value: "1"),
|
|
MoltenVKSettings(string: "MVK_CONFIG_LOG_LEVEL", value: "2"),
|
|
// MVK_CONFIG_LOG_LEVEL
|
|
//MVK_DEBUG
|
|
// Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64 or 192 depending on what i decide)
|
|
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "1024"),
|
|
]
|
|
|
|
_settings = State(initialValue: defaultSettings)
|
|
|
|
print("JIT Enabled: \(isJITEnabled)")
|
|
|
|
initializeSDL()
|
|
}
|
|
|
|
// MARK: - Body
|
|
var body: some View {
|
|
if game != nil, quits == false {
|
|
if isLoading {
|
|
if Air.shared.connected {
|
|
Text("")
|
|
.onAppear() {
|
|
Air.play(AnyView(emulationView))
|
|
}
|
|
} else {
|
|
|
|
emulationView
|
|
.onAppear() {
|
|
// This is fro the old exiting game feature that didn't work properly. will look into it and figure out a better alternative
|
|
/*
|
|
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
|
timer.invalidate()
|
|
quits = quit
|
|
|
|
if quits {
|
|
quit = false
|
|
timer.invalidate()
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
}
|
|
} else {
|
|
// This is when the game starts to stop the animation
|
|
EmulationView()
|
|
.onAppear() {
|
|
isAnimating = false
|
|
}
|
|
}
|
|
} else {
|
|
// This is the main menu view that includes the Settings and the Game Selector
|
|
mainMenuView
|
|
.onAppear() {
|
|
quits = false
|
|
|
|
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
private func initControllerObservers() {
|
|
NotificationCenter.default.addObserver(
|
|
forName: .GCControllerDidConnect,
|
|
object: nil,
|
|
queue: .main) { notification in
|
|
if let controller = notification.object as? GCController {
|
|
print("Controller connected: \(controller.productCategory)")
|
|
refreshControllersList()
|
|
}
|
|
}
|
|
|
|
|
|
NotificationCenter.default.addObserver(
|
|
forName: .GCControllerDidDisconnect,
|
|
object: nil,
|
|
queue: .main) { notification in
|
|
if let controller = notification.object as? GCController {
|
|
print("Controller disconnected: \(controller.productCategory)")
|
|
refreshControllersList()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - View Components
|
|
private var emulationView: some View {
|
|
GeometryReader { screenGeometry in
|
|
ZStack {
|
|
HStack(spacing: screenGeometry.size.width * 0.04) {
|
|
if let icon = game?.icon {
|
|
Image(uiImage: icon)
|
|
.resizable()
|
|
.frame(
|
|
width: min(screenGeometry.size.width * 0.25, 250),
|
|
height: min(screenGeometry.size.width * 0.25, 250)
|
|
)
|
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
.shadow(color: .black.opacity(0.5), radius: 10, x: 0, y: 5)
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: screenGeometry.size.height * 0.015) {
|
|
Text("Loading \(game?.titleName ?? "Game")")
|
|
.font(.system(size: min(screenGeometry.size.width * 0.04, 32)))
|
|
.foregroundColor(.white)
|
|
|
|
GeometryReader { geometry in
|
|
let containerWidth = min(screenGeometry.size.width * 0.35, 350)
|
|
|
|
ZStack(alignment: .leading) {
|
|
// Background track
|
|
Rectangle()
|
|
.cornerRadius(10)
|
|
.frame(width: containerWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
|
.foregroundColor(.gray.opacity(0.3))
|
|
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
|
|
|
|
// Animated loading bar
|
|
Rectangle()
|
|
.cornerRadius(10)
|
|
.frame(width: clumpWidth, height: min(screenGeometry.size.height * 0.015, 12))
|
|
.foregroundColor(.blue)
|
|
.shadow(color: .blue.opacity(0.5), radius: 4, x: 0, y: 2)
|
|
.offset(x: isAnimating ? containerWidth : -clumpWidth)
|
|
.animation(
|
|
Animation.linear(duration: 1.0)
|
|
.repeatForever(autoreverses: false),
|
|
value: isAnimating
|
|
)
|
|
}
|
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
|
.onAppear {
|
|
isAnimating = true
|
|
|
|
setupEmulation()
|
|
|
|
|
|
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
|
|
if get_current_fps() != 0 {
|
|
withAnimation {
|
|
isLoading = false
|
|
}
|
|
isAnimating = false
|
|
timer.invalidate()
|
|
}
|
|
print(get_current_fps())
|
|
}
|
|
}
|
|
}
|
|
.frame(height: min(screenGeometry.size.height * 0.015, 12))
|
|
.frame(width: min(screenGeometry.size.width * 0.35, 350))
|
|
}
|
|
}
|
|
.padding(.horizontal, screenGeometry.size.width * 0.06)
|
|
.padding(.vertical, screenGeometry.size.height * 0.05)
|
|
.position(
|
|
x: screenGeometry.size.width / 2,
|
|
y: screenGeometry.size.height * 0.5
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var mainMenuView: some View {
|
|
MainTabView(startemu: $game, config: $config, MVKconfig: $settings, controllersList: $controllersList, currentControllers: $currentControllers, onscreencontroller: $onscreencontroller)
|
|
.onAppear() {
|
|
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
|
|
refreshControllersList()
|
|
}
|
|
|
|
Air.play(AnyView(
|
|
VStack {
|
|
Image(systemName: "gamecontroller")
|
|
.font(.system(size: 300))
|
|
.foregroundColor(.gray)
|
|
.padding(.bottom, 10)
|
|
|
|
Text("Select Game")
|
|
.font(.system(size: 150))
|
|
.bold()
|
|
}
|
|
))
|
|
|
|
let isJIT = UserDefaults.standard.bool(forKey: "JIT-ENABLED")
|
|
|
|
if !isJIT, useTrollStore {
|
|
askForJIT()
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
var SdlInitFlags: uint = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO; // Initialises SDL2 for Events, Game Controller, Joystick, Audio and Video.
|
|
private func initializeSDL() {
|
|
setMoltenVKSettings()
|
|
SDL_SetMainReady() // Sets SDL Ready
|
|
SDL_iPhoneSetEventPump(SDL_TRUE) // Set iOS Event Pump to true (Check out SDL2 Documentation here)
|
|
SDL_Init(SdlInitFlags) // Initialises SDL2
|
|
initialize()
|
|
}
|
|
|
|
private func setupEmulation() {
|
|
patchMakeKeyAndVisible()
|
|
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
|
|
|
|
DispatchQueue.main.async {
|
|
start(displayid: 1)
|
|
}
|
|
}
|
|
|
|
private func refreshControllersList() {
|
|
controllersList = Ryujinx.shared.getConnectedControllers()
|
|
|
|
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
|
|
self.onscreencontroller = onscreen
|
|
}
|
|
|
|
controllersList.removeAll(where: { $0.id == "0"})
|
|
|
|
currentControllers = []
|
|
|
|
if controllersList.count == 1 {
|
|
let controller = controllersList[0]
|
|
currentControllers.append(controller)
|
|
} else if (controllersList.count - 1) >= 1 {
|
|
for controller in controllersList {
|
|
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
|
|
currentControllers.append(controller)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) {
|
|
DispatchQueue.main.async {
|
|
if let mainWindow = UIApplication.shared.windows.last {
|
|
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
|
|
|
if showOk {
|
|
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
|
|
completion(true)
|
|
}
|
|
|
|
alert.addAction(okAction)
|
|
} else {
|
|
completion(false)
|
|
}
|
|
|
|
mainWindow.rootViewController?.present(alert, animated: true, completion: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private func start(displayid: UInt32) {
|
|
guard let game else { return }
|
|
|
|
config.gamepath = game.fileURL.path
|
|
config.inputids = Array(Set(currentControllers.map(\.id)))
|
|
|
|
if mVKPreFillBuffer {
|
|
let setting = MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "2")
|
|
setenv(setting.string, setting.value, 1)
|
|
}
|
|
|
|
if config.inputids.isEmpty {
|
|
config.inputids.append("0")
|
|
}
|
|
|
|
do {
|
|
try Ryujinx.shared.start(with: config)
|
|
} catch {
|
|
print("Error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Sets MoltenVK Environment Variables
|
|
private func setMoltenVKSettings() {
|
|
settings.forEach { setting in
|
|
setenv(setting.string, setting.value, 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helper Functions
|
|
func loadSettings() -> Ryujinx.Configuration? {
|
|
guard let jsonString = UserDefaults.standard.string(forKey: "config"),
|
|
let data = jsonString.data(using: .utf8) else {
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
return try JSONDecoder().decode(Ryujinx.Configuration.self, from: data)
|
|
} catch {
|
|
print("Failed to load settings: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|