Updated JitStreamer Implementation, Reimplemented Texture Chunks, Reworked Alerts and more

This commit is contained in:
Stossy11 2025-03-20 21:33:28 +11:00
parent ceab2f0ac8
commit 54cb7eb953
30 changed files with 1074 additions and 486 deletions

View File

@ -41,4 +41,6 @@ ESCAPED_PATH=$(echo "$DOTNET_PATH" | sed 's/\//\\\//g')
# Update the xcconfig file
sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
$DOTNET_PATH clean
echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"

View File

@ -8,6 +8,6 @@
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
VERSION = 1.6.0
VERSION = 1.7.0
DOTNET = /usr/local/share/dotnet/dotnet

View File

@ -116,7 +116,7 @@
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib" = (
CodeSignOnCopy,
);
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework" = (
"Dependencies/Dynamic Libraries/RyujinxHelper.framework" = (
CodeSignOnCopy,
RemoveHeadersOnCopy,
);
@ -177,7 +177,7 @@
"Dependencies/Dynamic Libraries/libavutil.dylib",
"Dependencies/Dynamic Libraries/libMoltenVK.dylib",
"Dependencies/Dynamic Libraries/Ryujinx.Headless.SDL2.dylib",
"Dependencies/Dynamic Libraries/RyujinxKeyboard.framework",
"Dependencies/Dynamic Libraries/RyujinxHelper.framework",
Dependencies/XCFrameworks/libavcodec.xcframework,
Dependencies/XCFrameworks/libavfilter.xcframework,
Dependencies/XCFrameworks/libavformat.xcframework,
@ -484,6 +484,7 @@
/* Begin PBXTargetDependency section */
4E2953AC2D803BC9000497CD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
platformFilter = ios;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4E2953AB2D803BC9000497CD /* PBXContainerItemProxy */;
};
@ -712,6 +713,14 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
@ -842,10 +851,24 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
SWIFT_VERSION = 5.0;
@ -924,6 +947,14 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES;
@ -1054,10 +1085,24 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "MeloNX/App/Core/Headers/Ryujinx-Header.h";
SWIFT_VERSION = 5.0;

View File

@ -64,7 +64,10 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
allowLocationSimulation = "YES"
viewDebuggingEnabled = "No"
consoleMode = "0"
structuredConsoleMode = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference

View File

@ -6,40 +6,121 @@
//
import Foundation
import Network
import UIKit
func enableJITEB() {
guard let bundleID = Bundle.main.bundleIdentifier else {
return
func enableJITEB() {
if UserDefaults.standard.bool(forKey: "waitForVPN") {
waitForVPNConnection { connected in
if connected {
enableJITEBRequest()
}
}
} else {
enableJITEBRequest()
}
}
func enableJITEBRequest() {
let pid = Int(getpid())
print(pid)
let address = URL(string: "http://[fd00::]:9172/launch_app/\(bundleID)")!
let address = URL(string: "http://[fd00::]:9172/attach/\(pid)")!
var request = URLRequest(url: address)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.dataTask(with: address) { data, response, error in
if error != nil {
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
presentAlert(title: "Request Error", message: error.localizedDescription)
return
}
DispatchQueue.main.async {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let lastWindow = windowScene.windows.last {
showLaunchAppAlert(jsonData: data!, in: lastWindow.rootViewController!)
if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
showLaunchAppAlert(jsonData: data, in: windowScene.windows.last!.rootViewController!)
} else {
fatalError("Unable to get Window")
}
}
return
}
task.resume()
}
func waitForVPNConnection(timeout: TimeInterval = 30, interval: TimeInterval = 1, _ completion: @escaping (Bool) -> Void) {
let startTime = Date()
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
timer.schedule(deadline: .now(), repeating: interval)
timer.setEventHandler {
pingSite { connected in
if connected {
timer.cancel()
DispatchQueue.main.async {
completion(true)
}
} else if Date().timeIntervalSince(startTime) > timeout {
timer.cancel()
DispatchQueue.main.async {
completion(false)
}
}
}
}
timer.resume()
}
func pingSite(host: String = "http://[fd00::]:9172/hello", completion: @escaping (Bool) -> Void) {
guard let url = URL(string: host) else {
completion(false)
return
}
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 2.0
config.timeoutIntervalForResource = 2.0
let session = URLSession(configuration: config)
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = session.dataTask(with: request) { _, response, error in
if let error = error {
print("Ping failed: \(error.localizedDescription)")
completion(false)
} else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
completion(true)
} else {
let httpResponse = response as? HTTPURLResponse
completion(false)
}
}
task.resume()
}
func presentAlert(title: String, message: String, completion: (() -> Void)? = nil) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let lastWindow = windowScene.windows.last {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completion?()
})
DispatchQueue.main.async {
lastWindow.rootViewController?.present(alert, animated: true)
}
}
}
struct LaunchApp: Codable {
let ok: Bool
let error: String?
let launching: Bool
let position: Int?
let mounting: Bool
let success: Bool
let message: String
}
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
@ -48,28 +129,23 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
var message = ""
if let error = result.error {
message = "Error: \(error)"
} else if result.mounting {
message = "App is mounting..."
} else if result.launching {
message = "App is launching..."
if !result.success {
message += "\n\(result.message)"
let alert = UIAlertController(title: "JIT Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
DispatchQueue.main.async {
viewController.present(alert, animated: true)
}
} else {
message = "App launch status unknown."
}
if let position = result.position {
message += "\nPosition: \(position)"
}
let alert = UIAlertController(title: "Launch Status", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
DispatchQueue.main.async {
viewController.present(alert, animated: true)
print("Hopefully JIT is enabled now...")
Ryujinx.shared.ryuIsJITEnabled()
}
} catch {
print(String(data: jsonData, encoding: .utf8))
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))

View File

@ -189,6 +189,24 @@ enum VirtualControllerButton: Int {
case dPadRight
case leftTrigger
case rightTrigger
var isTrigger: Bool {
switch self {
case .leftTrigger, .rightTrigger, .leftShoulder, .rightShoulder:
return true
default:
return false
}
}
var isSmall: Bool {
switch self {
case .back, .start, .guide:
return true
default:
return false
}
}
}
enum ThumbstickType: Int {

View File

@ -31,7 +31,7 @@ struct iOSNav<Content: View>: View {
}
class Ryujinx {
class Ryujinx : ObservableObject {
private var isRunning = false
let virtualController = VirtualController()
@ -45,6 +45,10 @@ class Ryujinx {
@Published var defMLContentSize: CGFloat?
var thread: Thread!
@Published var jitenabled = false
var shouldMetal: Bool {
metalLayer == nil
}
@ -145,7 +149,7 @@ class Ryujinx {
self.config = config
RunLoop.current.perform { [self] in
thread = Thread { [self] in
isRunning = true
@ -178,6 +182,10 @@ class Ryujinx {
Self.log("Emulation failed to start: \(error)")
}
}
thread.qualityOfService = .background
thread.name = "MeloNX"
thread.start()
}
@ -192,6 +200,7 @@ class Ryujinx {
self.metalLayer = nil
stop_emulation()
thread.cancel()
}
var running: Bool {
@ -500,6 +509,11 @@ class Ryujinx {
static func log(_ message: String) {
print("[Ryujinx] \(message)")
}
func ryuIsJITEnabled() {
jitenabled = isJITEnabled()
print("JIT \(jitenabled)")
}
}

View File

@ -53,7 +53,7 @@ struct ContentView: View {
private let animationDuration: Double = 1.0
@State private var isAnimating = false
@State var isLoading = true
@State var jitNotEnabled = false
@StateObject var ryujinx = Ryujinx.shared
// MARK: - SDL
var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO
@ -79,14 +79,16 @@ struct ContentView: View {
_settings = State(initialValue: defaultSettings)
print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
initializeSDL()
}
// MARK: - Body
var body: some View {
if game != nil && (!jitNotEnabled || ignoreJIT) {
if game != nil && (ryujinx.jitenabled || ignoreJIT) {
gameView
} else if game != nil && jitNotEnabled {
} else if game != nil && !ryujinx.jitenabled {
jitErrorView
} else {
mainMenuView
@ -117,9 +119,16 @@ struct ContentView: View {
private var jitErrorView: some View {
Text("")
.sheet(isPresented: $jitNotEnabled) {
.sheet(isPresented:Binding(
get: { !ryujinx.jitenabled },
set: { newValue in
ryujinx.jitenabled = newValue
ryujinx.ryuIsJITEnabled()
})
) {
JITPopover() {
jitNotEnabled = false
ryujinx.jitenabled = false
}
.interactiveDismissDisabled()
}
@ -308,9 +317,9 @@ struct ContentView: View {
}
private func refreshControllersList() {
controllersList = Ryujinx.shared.getConnectedControllers()
controllersList = ryujinx.getConnectedControllers()
if let onscreen = controllersList.first(where: { $0.name == Ryujinx.shared.virtualController.controllername }) {
if let onscreen = controllersList.first(where: { $0.name == ryujinx.virtualController.controllername }) {
self.onscreencontroller = onscreen
}
@ -343,7 +352,7 @@ struct ContentView: View {
}
do {
try Ryujinx.shared.start(with: config)
try ryujinx.start(with: config)
} catch {
print("Error: \(error.localizedDescription)")
}
@ -351,7 +360,8 @@ struct ContentView: View {
private func configureEnvironmentVariables() {
if mVKPreFillBuffer {
setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
mVKPreFillBuffer = false
// setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
}
if syncqsubmits {
@ -366,8 +376,8 @@ struct ContentView: View {
}
private func checkJitStatus() {
jitNotEnabled = !isJITEnabled()
if jitNotEnabled {
ryujinx.ryuIsJITEnabled()
if !ryujinx.jitenabled {
if useTrollStore {
askForJIT()
} else if jitStreamerEB {
@ -382,9 +392,9 @@ struct ContentView: View {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleId == text })
game = ryujinx.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
game = Ryujinx.shared.games.first(where: { $0.titleName == text })
game = ryujinx.games.first(where: { $0.titleName == text })
}
}
}

View File

@ -11,101 +11,155 @@ import SwiftUIJoystick
import CoreMotion
struct ControllerView: View {
// MARK: - Properties
@AppStorage("On-ScreenControllerScale") private var controllerScale: Double = 1.0
@AppStorage("stick-button") private var stickButton = false
@State private var isPortrait = true
@Environment(\.verticalSizeClass) var verticalSizeClass
// MARK: - Body
var body: some View {
GeometryReader { geometry in
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
VStack {
Spacer()
VStack {
HStack {
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
Spacer()
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this works
ABXYView()
}
}
Group {
let isPad = UIDevice.current.userInterfaceIdiom == .pad
if isPortrait && !isPad {
portraitLayout
} else {
landscapeLayout
}
}
.padding()
.onChange(of: verticalSizeClass) { _ in
updateOrientation()
}
.onAppear(perform: updateOrientation)
}
// MARK: - Layouts
private var portraitLayout: some View {
VStack {
Spacer()
VStack(spacing: 20) {
HStack(spacing: 30) {
VStack(spacing: 15) {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
HStack {
ButtonView(button: .start) // Adding the + button
.padding(.horizontal, 40)
ButtonView(button: .back) // Adding the - button
.padding(.horizontal, 40)
}
VStack(spacing: 15) {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true)
ABXYView()
}
}
}
} else {
// could be landscape
VStack {
Spacer()
VStack {
HStack {
// gotta fuckin add + and - now
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
HStack {
// Spacer()
VStack {
// Spacer()
ButtonView(button: .back) // Adding the - button
}
Spacer()
VStack {
// Spacer()
ButtonView(button: .start) // Adding the + button
}
// Spacer()
}
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this work s
ABXYView()
}
}
}
HStack(spacing: 60) {
HStack {
ButtonView(button: .leftStick)
.padding()
ButtonView(button: .start)
}
HStack {
ButtonView(button: .back)
ButtonView(button: .rightStick)
.padding()
}
// .padding(.bottom, geometry.size.height / 11) // also extremally broken (
}
}
}
.padding()
}
private var landscapeLayout: some View {
VStack {
Spacer()
HStack {
VStack(spacing: 15) {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
Spacer()
centerButtons
Spacer()
VStack(spacing: 15) {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true)
ABXYView()
}
}
}
}
}
private var centerButtons: some View {
Group {
if stickButton {
VStack {
HStack(spacing: 50) {
ButtonView(button: .leftStick)
.padding()
Spacer()
ButtonView(button: .rightStick)
.padding()
}
.padding(.top, 30)
HStack(spacing: 50) {
ButtonView(button: .back)
Spacer()
ButtonView(button: .start)
}
}
.padding(.bottom, 20)
} else {
HStack(spacing: 50) {
ButtonView(button: .back)
Spacer()
ButtonView(button: .start)
}
.padding(.bottom, 20)
}
}
}
// MARK: - Methods
private func updateOrientation() {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
isPortrait = window.bounds.size.height > window.bounds.size.width
}
}
}
struct ShoulderButtonsViewLeft: View {
@State var width: CGFloat = 160
@State var height: CGFloat = 20
@State private var width: CGFloat = 160
@State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack {
HStack(spacing: 20) {
ButtonView(button: .leftTrigger)
.padding(.horizontal)
ButtonView(button: .leftShoulder)
.padding(.horizontal)
}
.frame(width: width, height: height)
.onAppear() {
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
@ -118,19 +172,17 @@ struct ShoulderButtonsViewLeft: View {
}
struct ShoulderButtonsViewRight: View {
@State var width: CGFloat = 160
@State var height: CGFloat = 20
@State private var width: CGFloat = 160
@State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
HStack {
HStack(spacing: 20) {
ButtonView(button: .rightShoulder)
.padding(.horizontal)
ButtonView(button: .rightTrigger)
.padding(.horizontal)
}
.frame(width: width, height: height)
.onAppear() {
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
@ -143,21 +195,21 @@ struct ShoulderButtonsViewRight: View {
}
struct DPadView: View {
@State var size: CGFloat = 145
@State private var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack {
VStack(spacing: 5) {
ButtonView(button: .dPadUp)
HStack {
HStack(spacing: 20) {
ButtonView(button: .dPadLeft)
Spacer(minLength: 20)
ButtonView(button: .dPadRight)
}
ButtonView(button: .dPadDown)
.padding(.horizontal)
}
.frame(width: size, height: size)
.onAppear() {
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
@ -168,22 +220,21 @@ struct DPadView: View {
}
struct ABXYView: View {
@State var size: CGFloat = 145
@State private var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View {
VStack {
VStack(spacing: 5) {
ButtonView(button: .X)
HStack {
HStack(spacing: 20) {
ButtonView(button: .Y)
Spacer(minLength: 20)
ButtonView(button: .A)
}
ButtonView(button: .B)
.padding(.horizontal)
}
.frame(width: size, height: size)
.onAppear() {
.onAppear {
if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2
}
@ -195,58 +246,90 @@ struct ABXYView: View {
struct ButtonView: View {
var button: VirtualControllerButton
@State var width: CGFloat = 45
@State var height: CGFloat = 45
@State var isPressed = false
@State private var width: CGFloat = 45
@State private var height: CGFloat = 45
@State private var isPressed = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State private var debounceTimer: Timer?
var body: some View {
Image(systemName: buttonText)
.resizable()
.scaledToFit()
.frame(width: width, height: height)
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
.opacity(isPressed ? 0.4 : 0.7)
.foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9))
.background(
Group {
if !button.isTrigger {
Circle()
.fill(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
.frame(width: width * 1.25, height: height * 1.25)
} else {
Image(systemName: buttonText)
.resizable()
.scaledToFit()
.frame(width: width * 1.25, height: height * 1.25)
.foregroundColor(true ? Color.gray.opacity(0.4) : Color.gray.opacity(0.3))
}
}
)
.opacity(isPressed ? 0.6 : 1.0)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
if !self.isPressed {
self.isPressed = true
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.heavy)
}
handleButtonPress()
}
.onEnded { _ in
self.isPressed = false
Ryujinx.shared.virtualController.setButtonState(0, for: button)
handleButtonRelease()
}
)
.onAppear() {
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder {
width = 65
}
if button == .back || button == .start || button == .guide {
width = 35
height = 35
}
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
)
.onAppear {
configureSizeForButton()
}
}
private func handleButtonPress() {
if !isPressed {
isPressed = true
debounceTimer?.invalidate()
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.medium)
}
}
private func handleButtonRelease() {
if isPressed {
isPressed = false
debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { _ in
Ryujinx.shared.virtualController.setButtonState(0, for: button)
}
}
}
private func configureSizeForButton() {
if button.isTrigger {
width = 70
height = 40
} else if button.isSmall {
width = 35
height = 35
}
// Adjust for iPad
if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2
height *= 1.2
}
width *= CGFloat(controllerScale)
height *= CGFloat(controllerScale)
}
private var buttonText: String {
switch button {
@ -258,6 +341,10 @@ struct ButtonView: View {
return "x.circle.fill"
case .Y:
return "y.circle.fill"
case .leftStick:
return "l.joystick.press.down.fill"
case .rightStick:
return "r.joystick.press.down.fill"
case .dPadUp:
return "arrowtriangle.up.circle.fill"
case .dPadDown:
@ -267,7 +354,7 @@ struct ButtonView: View {
case .dPadRight:
return "arrowtriangle.right.circle.fill"
case .leftTrigger:
return"zl.rectangle.roundedtop.fill"
return "zl.rectangle.roundedtop.fill"
case .rightTrigger:
return "zr.rectangle.roundedtop.fill"
case .leftShoulder:
@ -275,16 +362,11 @@ struct ButtonView: View {
case .rightShoulder:
return "r.rectangle.roundedbottom.fill"
case .start:
return "plus.circle.fill" // System symbol for +
return "plus.circle.fill"
case .back:
return "minus.circle.fill" // System symbol for -
return "minus.circle.fill"
case .guide:
return "house.circle.fill"
// This should be all the cases
default:
return ""
}
}
}

View File

@ -11,7 +11,7 @@ import SwiftUIJoystick
public struct Joystick: View {
@State var iscool: Bool? = nil
@Environment(\.colorScheme) var colorScheme
@ObservedObject public var joystickMonitor = JoystickMonitor()
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var dragDiameter: CGFloat {
@ -36,8 +36,13 @@ public struct Joystick: View {
.hidden()
},
foreground: {
Circle().fill(Color.gray)
.opacity(0.7)
Circle()
.fill(colorScheme == .dark ? Color.white.opacity(0.7) : Color.black.opacity(0.7))
.background(
Circle()
.fill(colorScheme == .dark ? Color.gray.opacity(0.3) : Color.gray.opacity(0.2))
.frame(width: (dragDiameter / 4) * 1.2, height: (dragDiameter / 4) * 1.2)
)
},
locksInPlace: false)
.onChange(of: self.joystickMonitor.xyPoint) { newValue in

View File

@ -29,6 +29,7 @@ struct GameLibraryView: View {
@State var isViewingGameInfo: Bool = false
@State var isSelectingGameUpdate: Bool = false
@State var isSelectingGameDLC: Bool = false
@StateObject var ryujinx = Ryujinx.shared
@State var gameInfo: Game?
var games: Binding<[Game]> {
Binding(
@ -203,6 +204,13 @@ struct GameLibraryView: View {
.foregroundColor(.blue)
}
}
ToolbarItem(placement: .topBarLeading) {
if ryujinx.jitenabled {
Image(systemName: "checkmark")
.foregroundStyle(.green)
}
}
}
.onChange(of: startemu) { game in
guard let game else { return }

View File

@ -37,6 +37,8 @@ struct JITPopover: View {
if isJIT {
dismiss()
onJITEnabled()
Ryujinx.shared.ryuIsJITEnabled()
}
}
}

View File

@ -96,13 +96,14 @@ struct LogFileView: View {
private func startLogFileWatching() {
showingLogs = true
self.readLatestLogFile()
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
if showingLogs {
self.readLatestLogFile()
}
if isfps {
sleep(1)
if get_current_fps() != 0 {
stopLogFileWatching()
timer.invalidate()

View File

@ -48,11 +48,15 @@ struct SettingsView: View {
@AppStorage("showlogsgame") var showlogsgame: Bool = false
@AppStorage("stick-button") var stickButton = false
@AppStorage("waitForVPN") var waitForVPN = false
@State private var showResolutionInfo = false
@State private var showAnisotropicInfo = false
@State private var showControllerInfo = false
@State private var searchText = ""
@AppStorage("portal") var gamepo = false
@StateObject var ryujinx = Ryujinx.shared
var filteredMemoryModes: [(String, String)] {
guard !searchText.isEmpty else { return memoryManagerModes }
@ -286,6 +290,11 @@ struct SettingsView: View {
}.tint(.blue)
Toggle(isOn: $stickButton) {
labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down")
}.tint(.blue)
Toggle(isOn: $ryuDemo) {
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
}
@ -451,10 +460,22 @@ struct SettingsView: View {
}
.tint(.blue)
.contextMenu {
Button {
waitForVPN.toggle()
} label: {
Label {
Text("Wait for VPN")
} icon: {
if waitForVPN {
Image(systemName: "checkmark")
}
}
}
Button {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let mainWindow = windowScene.windows.last {
let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best iOS developers of all time jkcoxson <3", preferredStyle: .alert)
let alertController = UIAlertController(title: "About JitStreamer EB", message: "JitStreamer EB is an Amazing Application to Enable JIT on the go, made by one of the best, most kind, helpful and nice developers of all time jkcoxson <3", preferredStyle: .alert)
let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
@ -539,7 +560,11 @@ struct SettingsView: View {
model.hasPrefix("iPhone") ? "iphone" :
"macwindow"
labelWithIcon("JIT Acquisition: \(isJITEnabled() ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
labelWithIcon("JIT Acquisition: \(ryujinx.jitenabled ? "Acquired" : "Not Acquired" )", iconName: "bolt.fill")
.onAppear() {
print("JIY ;(((((")
ryujinx.ryuIsJITEnabled()
}
labelWithIcon("Increased Memory Limit Entitlement: \(checkAppEntitlement("com.apple.developer.kernel.increased-memory-limit") ? "Enabled" : "Disabled")", iconName: "memorychip")
@ -566,10 +591,6 @@ struct SettingsView: View {
Section {
DisclosureGroup {
Toggle(isOn: $mVKPreFillBuffer) {
labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
}.tint(.blue)
Toggle(isOn: $config.dfsIntegrityChecks) {
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
}.tint(.blue)
@ -637,6 +658,8 @@ struct SettingsView: View {
.navigationBarTitleDisplayMode(.inline)
.listStyle(.insetGrouped)
.onAppear {
mVKPreFillBuffer = false
if let configs = loadSettings() {
self.config = configs
} else {

View File

@ -8,6 +8,7 @@
import SwiftUI
import UniformTypeIdentifiers
// MARK: - Models
struct DownloadableContentNca: Codable, Hashable {
var fullPath: String
var titleId: UInt
@ -20,9 +21,18 @@ struct DownloadableContentNca: Codable, Hashable {
}
}
struct DownloadableContentContainer: Codable, Hashable {
struct DownloadableContentContainer: Codable, Hashable, Identifiable {
var id: String { containerPath }
var containerPath: String
var downloadableContentNcaList: [DownloadableContentNca]
var filename: String {
(containerPath as NSString).lastPathComponent
}
var isEnabled: Bool {
downloadableContentNcaList.first?.enabled == true
}
enum CodingKeys: String, CodingKey {
case containerPath = "path"
@ -30,136 +40,265 @@ struct DownloadableContentContainer: Codable, Hashable {
}
}
// MARK: - View
struct DLCManagerSheet: View {
// MARK: - Properties
@Binding var game: Game!
@State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = []
@Environment(\.dismiss) private var dismiss
// MARK: - Body
var body: some View {
NavigationView {
let withIndex = dlcs.enumerated().map { $0 }
List(withIndex, id: \.element.containerPath) { index, dlc in
Button(action: {
let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true
dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle }
Self.saveDlcs(game, dlc: dlcs)
}) {
HStack {
Text((dlc.containerPath as NSString).lastPathComponent)
.foregroundStyle(Color(uiColor: .label))
Spacer()
if dlc.downloadableContentNcaList.first?.enabled == true {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
.font(.system(size: 24))
} else {
Image(systemName: "circle")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.font(.system(size: 24))
}
}
}
.contextMenu {
Button {
let path = URL.documentsDirectory.appendingPathComponent(dlc.containerPath)
try? FileManager.default.removeItem(atPath: path.path)
dlcs.remove(at: index)
Self.saveDlcs(game, dlc: dlcs)
} label: {
Text("Remove DLC")
iOSNav {
List {
if dlcs.isEmpty {
emptyStateView
} else {
ForEach(dlcs) { dlc in
dlcRow(dlc)
}
.onDelete(perform: removeDLCs)
}
}
.navigationTitle("\(game.titleName) DLCs")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Add", systemImage: "plus") {
isSelectingGameDLC = true
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isSelectingGameDLC = true
} label: {
Label("Add DLC", systemImage: "plus")
}
}
}
.onAppear {
loadData()
}
}
.onAppear {
dlcs = Self.loadDlc(game)
}
.fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in
switch result {
case .success(let urls):
for url in urls {
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let dlcDirectory = documentsDirectory.appendingPathComponent("dlc")
let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
if !fileManager.fileExists(atPath: dlcDirectory.path) {
try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil)
}
if !fileManager.fileExists(atPath: romDlcDirectory.path) {
try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil)
}
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path)
guard !dlcContent.isEmpty else { return }
let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL)
let container = DownloadableContentContainer(
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
downloadableContentNcaList: dlcContent
)
dlcs.append(container)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Error copying game file: \(error)")
}
.fileImporter(
isPresented: $isSelectingGameDLC,
allowedContentTypes: [.item],
allowsMultipleSelection: true,
onCompletion: handleFileImport
)
}
// MARK: - Views
private var emptyStateView: some View {
Group {
if #available(iOS 17, *) {
ContentUnavailableView(
"No DLCs Found",
systemImage: "puzzlepiece.extension",
description: Text("Tap the + button to add game DLCs.")
)
} else {
VStack(spacing: 20) {
Spacer()
Image(systemName: "puzzlepiece.extension")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No DLCs Found")
.font(.title2)
.fontWeight(.semibold)
Text("Tap the + button to add game DLCs.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Spacer()
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
.frame(maxWidth: .infinity)
.listRowInsets(EdgeInsets())
}
}
}
private func dlcRow(_ dlc: DownloadableContentContainer) -> some View {
Button {
toggleDLC(dlc)
} label: {
HStack {
Text(dlc.filename)
.foregroundStyle(.primary)
Spacer()
Image(systemName: dlc.isEnabled ? "checkmark.circle.fill" : "circle")
.foregroundStyle(dlc.isEnabled ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
if let index = dlcs.firstIndex(where: { $0.id == dlc.id }) {
removeDLC(at: IndexSet(integer: index))
}
} label: {
Label("Delete", systemImage: "trash")
}
}
}
// MARK: - Functions
private func loadData() {
dlcs = Self.loadDlc(game)
}
private func toggleDLC(_ dlc: DownloadableContentContainer) {
guard let index = dlcs.firstIndex(where: { $0.id == dlc.id }) else { return }
let toggle = !dlcs[index].isEnabled
dlcs[index].downloadableContentNcaList = dlcs[index].downloadableContentNcaList.map { nca in
var mutableNca = nca
mutableNca.enabled = toggle
return mutableNca
}
Self.saveDlcs(game, dlc: dlcs)
}
private func removeDLCs(at offsets: IndexSet) {
offsets.forEach { removeDLC(at: IndexSet(integer: $0)) }
}
private func removeDLC(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let dlcToRemove = dlcs[index]
let path = URL.documentsDirectory.appendingPathComponent(dlcToRemove.containerPath)
do {
try FileManager.default.removeItem(at: path)
dlcs.remove(at: index)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Failed to remove DLC: \(error)")
}
}
private func handleFileImport(result: Result<[URL], Error>) {
switch result {
case .success(let urls):
for url in urls {
importDLC(from: url)
}
case .failure(let error):
print("File import failed: \(error.localizedDescription)")
}
}
private func importDLC(from url: URL) {
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let dlcDirectory = URL.documentsDirectory.appendingPathComponent("dlc")
let gameDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId)
try fileManager.createDirectory(at: gameDlcDirectory, withIntermediateDirectories: true)
// Copy the DLC file
let destinationURL = gameDlcDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.removeItem(at: destinationURL)
try fileManager.copyItem(at: url, to: destinationURL)
// Fetch DLC metadata from Ryujinx
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: destinationURL.path)
guard !dlcContent.isEmpty else {
print("No valid DLC content found")
return
}
let newDlcContainer = DownloadableContentContainer(
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
downloadableContentNcaList: dlcContent
)
dlcs.append(newDlcContainer)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Error importing DLC: \(error)")
}
}
}
// MARK: - Helper Methods
private extension DLCManagerSheet {
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
let jsonURL = dlcJsonPath(for: game)
try? FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
guard let data = try? Data(contentsOf: jsonURL),
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
else { return [] }
result = result.filter { container in
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
return FileManager.default.fileExists(atPath: path.path)
do {
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
guard FileManager.default.fileExists(atPath: jsonURL.path),
let data = try? Data(contentsOf: jsonURL),
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
else { return [] }
result = result.filter { container in
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath)
return FileManager.default.fileExists(atPath: path.path)
}
return result
} catch {
print("Error loading DLCs: \(error)")
return []
}
return result
}
static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
guard let data = try? JSONEncoder().encode(dlc) else { return }
try? data.write(to: dlcJsonPath(for: game))
do {
let data = try JSONEncoder().encode(dlc)
try data.write(to: dlcJsonPath(for: game))
} catch {
print("Error saving DLCs: \(error)")
}
}
static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
}
static func dlcJsonPath(for game: Game) -> URL {
URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game.titleId).appendingPathComponent("dlc.json")
URL.documentsDirectory
.appendingPathComponent("games")
.appendingPathComponent(game.titleId)
.appendingPathComponent("dlc.json")
}
}
// MARK: - Array Extension
extension Array where Element: AnyObject {
mutating func mutableForEach(_ body: (inout Element) -> Void) {
for index in indices {
var element = self[index]
body(&element)
self[index] = element
}
}
}
// MARK: - URL Extension
extension URL {
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
static var documentsDirectory: URL {

View File

@ -9,149 +9,169 @@ import SwiftUI
import UniformTypeIdentifiers
struct UpdateManagerSheet: View {
@State private var items: [String] = []
@State private var paths: [URL] = []
@State private var selectedItem: String? = nil
// MARK: - Properties
@State private var updates: [UpdateItem] = []
@Binding var game: Game?
@State private var isSelectingGameUpdate = false
@State private var jsonURL: URL? = nil
@Environment(\.dismiss) private var dismiss
// MARK: - Models
class UpdateItem: Identifiable, ObservableObject {
let id = UUID()
let url: URL
let filename: String
let path: String
@Published var isSelected: Bool = false
init(url: URL, filename: String, path: String, isSelected: Bool = false) {
self.url = url
self.filename = filename
self.path = path
self.isSelected = isSelected
}
}
// MARK: - Body
var body: some View {
NavigationView {
List(paths, id: \..self, selection: $selectedItem) { item in
Button(action: {
selectItem(item.lastPathComponent)
}) {
HStack {
Text(item.lastPathComponent)
.foregroundStyle(Color(uiColor: .label))
Spacer()
if selectedItem == "updates/\(game!.titleId)/\(item.lastPathComponent)" {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
.font(.system(size: 24))
} else {
Image(systemName: "circle")
.foregroundStyle(Color(uiColor: .secondaryLabel))
.font(.system(size: 24))
}
iOSNav {
List {
if updates.isEmpty {
emptyStateView
} else {
ForEach(updates) { update in
updateRow(update)
}
.onDelete(perform: removeUpdates)
}
}
.navigationTitle("\(game?.titleName ?? "Game") Updates")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Done") {
dismiss()
}
}
.contextMenu {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
removeUpdate(item)
isSelectingGameUpdate = true
} label: {
Text("Remove Update")
Label("Add Update", systemImage: "plus")
}
}
}
.onAppear {
print(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
loadJSON(URL.documentsDirectory.appendingPathComponent("games").appendingPathComponent(game!.titleId).appendingPathComponent("updates.json"))
}
.navigationTitle("\(game!.titleName) Updates")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Add", systemImage: "plus") {
isSelectingGameUpdate = true
}
loadData()
}
}
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item]) { result in
switch result {
case .success(let url):
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
defer { url.stopAccessingSecurityScopedResource() }
let gameInfo = game!
do {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let updatedDirectory = documentsDirectory.appendingPathComponent("updates")
let romUpdatedDirectory = updatedDirectory.appendingPathComponent(gameInfo.titleId)
.fileImporter(isPresented: $isSelectingGameUpdate, allowedContentTypes: [.item], onCompletion: handleFileImport)
}
// MARK: - Views
private var emptyStateView: some View {
Group {
if #available(iOS 17, *) {
ContentUnavailableView(
"No Updates Found",
systemImage: "arrow.down.circle",
description: Text("Tap the + button to add game updates.")
)
} else {
VStack(spacing: 20) {
Spacer()
if !fileManager.fileExists(atPath: updatedDirectory.path) {
try fileManager.createDirectory(at: updatedDirectory, withIntermediateDirectories: true, attributes: nil)
}
if !fileManager.fileExists(atPath: romUpdatedDirectory.path) {
try fileManager.createDirectory(at: romUpdatedDirectory, withIntermediateDirectories: true, attributes: nil)
}
let destinationURL = romUpdatedDirectory.appendingPathComponent(url.lastPathComponent)
try? fileManager.copyItem(at: url, to: destinationURL)
items.append("updates/" + gameInfo.titleId + "/" + url.lastPathComponent)
selectItem(url.lastPathComponent)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
loadJSON(jsonURL!)
} catch {
print("Error copying game file: \(error)")
Image(systemName: "arrow.down.circle")
.font(.system(size: 64))
.foregroundColor(.secondary)
Text("No Updates Found")
.font(.title2)
.fontWeight(.semibold)
Text("Tap the + button to add game updates.")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Spacer()
}
case .failure(let err):
print("File import failed: \(err.localizedDescription)")
.frame(maxWidth: .infinity)
.listRowInsets(EdgeInsets())
}
}
}
func removeUpdate(_ game: URL) {
let gameString = "updates/\(self.game!.titleId)/\(game.lastPathComponent)"
paths.removeAll { $0 == game }
items.removeAll { $0 == gameString }
if selectedItem == gameString {
selectedItem = nil
private func updateRow(_ update: UpdateItem) -> some View {
Button {
toggleSelection(update)
} label: {
HStack {
Text(update.filename)
.foregroundStyle(.primary)
Spacer()
Image(systemName: update.isSelected ? "checkmark.circle.fill" : "circle")
.foregroundStyle(update.isSelected ? .primary : .secondary)
.imageScale(.large)
}
.contentShape(Rectangle())
}
do {
try FileManager.default.removeItem(at: game)
} catch {
print(error)
.buttonStyle(.plain)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
if let index = updates.firstIndex(where: { $0.path == update.path }) {
removeUpdate(at: IndexSet(integer: index))
}
} label: {
Label("Delete", systemImage: "trash")
}
}
saveJSON(selectedItem: selectedItem ?? "")
Ryujinx.shared.games = Ryujinx.shared.loadGames()
}
func saveJSON(selectedItem: String?) {
// MARK: - Functions
private func loadData() {
guard let game = game else { return }
let documentsDirectory = URL.documentsDirectory
jsonURL = documentsDirectory
.appendingPathComponent("games")
.appendingPathComponent(game.titleId)
.appendingPathComponent("updates.json")
loadJSON()
}
private func loadJSON() {
guard let jsonURL = jsonURL else { return }
do {
let jsonDict = ["paths": items, "selected": selectedItem ?? self.selectedItem ?? ""] as [String: Any]
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL)
} catch {
print("Failed to update JSON: \(error)")
}
}
func loadJSON(_ json: URL) {
self.jsonURL = json
guard let jsonURL else { return }
do {
if !FileManager.default.fileExists(atPath: jsonURL.path) {
createDefaultJSON()
return
}
let data = try Data(contentsOf: jsonURL)
if let jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let list = jsonDict["paths"] as? [String]
{
let filteredList = list.filter { relativePath in
let paths = jsonDict["paths"] as? [String],
let selected = jsonDict["selected"] as? String {
let filteredPaths = paths.filter { relativePath in
let path = URL.documentsDirectory.appendingPathComponent(relativePath)
return FileManager.default.fileExists(atPath: path.path)
}
let urls: [URL] = filteredList.map { relativePath in
URL.documentsDirectory.appendingPathComponent(relativePath)
updates = filteredPaths.map { relativePath in
let url = URL.documentsDirectory.appendingPathComponent(relativePath)
return UpdateItem(
url: url,
filename: url.lastPathComponent,
path: relativePath,
isSelected: selected == relativePath
)
}
items = filteredList
paths = urls
selectedItem = jsonDict["selected"] as? String
}
} catch {
print("Failed to read JSON: \(error)")
@ -159,42 +179,119 @@ struct UpdateManagerSheet: View {
}
}
func createDefaultJSON() {
private func createDefaultJSON() {
guard let jsonURL = jsonURL else { return }
let defaultData: [String: Any] = ["selected": "", "paths": []]
do {
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
let defaultData: [String: Any] = ["selected": "", "paths": []]
let newData = try JSONSerialization.data(withJSONObject: defaultData, options: .prettyPrinted)
try newData.write(to: jsonURL)
items = []
selectedItem = ""
updates = []
} catch {
print("Failed to create default JSON: \(error)")
}
}
func selectItem(_ item: String) {
let newSelection = "updates/\(game!.titleId)/\(item)"
guard let jsonURL else { return }
do {
let data = try Data(contentsOf: jsonURL)
try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
var jsonDict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
if let currentSelected = jsonDict["selected"] as? String, currentSelected == newSelection {
jsonDict["selected"] = ""
selectedItem = ""
} else {
jsonDict["selected"] = "\(newSelection)"
selectedItem = newSelection
private func handleFileImport(result: Result<URL, Error>) {
switch result {
case .success(let selectedURL):
guard let game = game,
selectedURL.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource")
return
}
jsonDict["paths"] = items
defer { selectedURL.stopAccessingSecurityScopedResource() }
do {
let fileManager = FileManager.default
let updatesDirectory = URL.documentsDirectory.appendingPathComponent("updates")
let gameUpdatesDirectory = updatesDirectory.appendingPathComponent(game.titleId)
// Create directories if needed
try fileManager.createDirectory(at: gameUpdatesDirectory, withIntermediateDirectories: true)
// Copy the file
let destinationURL = gameUpdatesDirectory.appendingPathComponent(selectedURL.lastPathComponent)
try? fileManager.removeItem(at: destinationURL) // Remove if exists
try fileManager.copyItem(at: selectedURL, to: destinationURL)
// Add to updates
let relativePath = "updates/\(game.titleId)/\(selectedURL.lastPathComponent)"
let newUpdate = UpdateItem(
url: destinationURL,
filename: selectedURL.lastPathComponent,
path: relativePath
)
updates.append(newUpdate)
toggleSelection(newUpdate)
// Reload games
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Error copying update file: \(error)")
}
case .failure(let error):
print("File import failed: \(error.localizedDescription)")
}
}
private func toggleSelection(_ update: UpdateItem) {
print("toggle selection \(update.path)")
updates = updates.map { item in
var mutableItem = item
mutableItem.isSelected = item.path == update.path && !update.isSelected
print(mutableItem.isSelected)
print(update.isSelected)
return mutableItem
}
print(updates)
saveJSON()
}
private func removeUpdates(at offsets: IndexSet) {
offsets.forEach { removeUpdate(at: IndexSet(integer: $0)) }
}
private func removeUpdate(at indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let updateToRemove = updates[index]
do {
// Remove the file
try FileManager.default.removeItem(at: updateToRemove.url)
// Remove from updates array
updates.remove(at: index)
// Save changes
saveJSON()
// Reload games
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Failed to remove update: \(error)")
}
}
private func saveJSON() {
guard let jsonURL = jsonURL else { return }
do {
let paths = updates.map { $0.path }
let selected = updates.first(where: { $0.isSelected })?.path ?? ""
let jsonDict = ["paths": paths, "selected": selected] as [String: Any]
let newData = try JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
try newData.write(to: jsonURL)
Ryujinx.shared.games = Ryujinx.shared.loadGames()
} catch {
print("Failed to update JSON: \(error)")
}

View File

@ -21,7 +21,7 @@ struct SetupView: View {
var body: some View {
iOSNav {
ZStack {
if UIDevice.current.userInterfaceIdiom == .pad {
if UIDevice.current.systemName.contains("iPadOS") {
iPadSetupView(
finished: $finished,
isImportingKeys: $isImportingKeys,
@ -65,8 +65,9 @@ struct SetupView: View {
initialize()
finished = false
keysImported = Ryujinx.shared.checkIfKeysImported()
print((Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0))
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmImported = (firmware == "" ? "0" : firmware) != "0"
}
}
@ -369,9 +370,8 @@ struct SetupView: View {
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
print(Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0)
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmImported = (firmware == "" ? "0" : firmware) != "0"
alertMessage = "Firmware installed successfully"
showAlert = true

View File

@ -0,0 +1,6 @@
framework module RyujinxHelper {
umbrella header "RyujinxHelper.h"
export *
module * { export * }
}

View File

@ -4,22 +4,22 @@
<dict>
<key>files</key>
<dict>
<key>Headers/RyujinxKeyboard.h</key>
<key>Headers/RyujinxHelper.h</key>
<data>
5P7GN4g050n199pV6/+SpfMBgJc=
</data>
<key>Info.plist</key>
<data>
hYdI/ktAKwjBSfaJpt6Yc8UKLCY=
UOH9NuuEcz5NQiQlrM2LNFaG2pI=
</data>
<key>Modules/module.modulemap</key>
<data>
0kFAMoTn+4Q1J/dM6uMLe3EhbL0=
JDij7psMD6pZZpigUfkSQldib+I=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/RyujinxKeyboard.h</key>
<key>Headers/RyujinxHelper.h</key>
<dict>
<key>hash2</key>
<data>
@ -30,7 +30,7 @@
<dict>
<key>hash2</key>
<data>
K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ=
5t/lQcpkzC5bwJqFQqIf6h1ldlhHouYzDawRVrnUeyM=
</data>
</dict>
</dict>

View File

@ -1,6 +0,0 @@
framework module RyujinxKeyboard {
umbrella header "RyujinxKeyboard.h"
export *
module * { export * }
}

View File

@ -769,7 +769,7 @@ namespace Ryujinx.Graphics.Vulkan
private void SetData(ReadOnlySpan<byte> data, int layer, int level, int layers, int levels, bool singleSlice, Rectangle<int>? region = null)
{
const int MaxChunkSize = 1024 * 1024 * 16; // 16MB chunks
const int MaxChunkSize = 1024 * 1024;
int bufferDataLength = GetBufferDataLength(data.Length);
@ -786,21 +786,39 @@ namespace Ryujinx.Graphics.Vulkan
for (int i = 0; i < layers; i++)
{
int currentLayer = layer + i;
int currentLayerSize = Math.Min(layerSize, data.Length - offset);
var layerData = data.Slice(offset, currentLayerSize);
ProcessChunk(layerData, currentLayer, level, 1, levels, true);
offset += layerSize;
if (offset >= data.Length)
break;
int currentLayer = layer + i;
int currentLayerSize = Math.Min(layerSize, data.Length - offset);
if (currentLayerSize <= 0)
break;
try
{
var layerData = data.Slice(offset, currentLayerSize);
ProcessChunk(layerData, currentLayer, level, 1, levels, true);
offset += layerSize;
}
catch (ArgumentOutOfRangeException)
{
break;
}
}
}
else if (region.HasValue)
{
var rect = region.Value;
if (rect.Width <= 0 || rect.Height <= 0)
return;
int dataPerPixel = data.Length / (rect.Width * rect.Height);
if (dataPerPixel <= 0)
return;
int rowStride = rect.Width * dataPerPixel;
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
@ -811,42 +829,63 @@ namespace Ryujinx.Graphics.Vulkan
while (currentY < rect.Y + originalHeight)
{
int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY);
if (chunkHeight <= 0)
break;
var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight);
int chunkSize = chunkHeight * rowStride;
int safeChunkSize = Math.Min(chunkSize, data.Length - offset);
var chunkData = data.Slice(offset, safeChunkSize);
ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion);
currentY += chunkHeight;
offset += chunkSize;
if (offset >= data.Length)
break;
int safeChunkSize = Math.Min(chunkSize, data.Length - offset);
if (safeChunkSize <= 0)
break;
try
{
var chunkData = data.Slice(offset, safeChunkSize);
ProcessChunk(chunkData, layer, level, 1, 1, true, chunkRegion);
currentY += chunkHeight;
offset += chunkSize;
}
catch (ArgumentOutOfRangeException)
{
break;
}
}
}
else
{
ProcessChunk(data, layer, level, layers, levels, singleSlice, region);
}
}
private void ProcessChunk(ReadOnlySpan<byte> chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle<int>? chunkRegion = null)
{
int chunkBufferLength = GetBufferDataLength(chunkData.Length);
void ProcessChunk(ReadOnlySpan<byte> chunkData, int chunkLayer, int chunkLevel, int chunkLayers, int chunkLevels, bool chunkSingleSlice, Rectangle<int>? chunkRegion = null)
if (chunkBufferLength <= 0)
return;
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength);
using (var imageAuto = GetImage())
{
int chunkBufferLength = GetBufferDataLength(chunkData.Length);
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
using var bufferHolder = _gd.BufferManager.Create(_gd, chunkBufferLength);
using (var imageAuto = GetImage())
if (loadInline)
{
_gd.PipelineInternal.EndRenderPass();
}
try
{
bool loadInline = Storage.HasCommandBufferDependency(_gd.PipelineInternal.CurrentCommandBuffer);
var cbs = loadInline ? _gd.PipelineInternal.CurrentCommandBuffer : _gd.PipelineInternal.GetPreloadCommandBuffer();
if (loadInline)
{
_gd.PipelineInternal.EndRenderPass();
}
CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData);
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
@ -854,6 +893,11 @@ namespace Ryujinx.Graphics.Vulkan
if (chunkRegion.HasValue)
{
var region = chunkRegion.Value;
if (region.Width <= 0 || region.Height <= 0)
return;
CopyFromOrToBuffer(
cbs.CommandBuffer,
buffer,
@ -862,10 +906,10 @@ namespace Ryujinx.Graphics.Vulkan
false,
chunkLayer,
chunkLevel,
chunkRegion.Value.X,
chunkRegion.Value.Y,
chunkRegion.Value.Width,
chunkRegion.Value.Height);
region.X,
region.Y,
region.Width,
region.Height);
}
else
{
@ -881,7 +925,11 @@ namespace Ryujinx.Graphics.Vulkan
chunkLevels,
chunkSingleSlice);
}
}
}
catch (Exception e)
{
}
}
}

View File

@ -239,6 +239,7 @@ namespace Ryujinx.HLE.HOS.Applets
StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
InitialText = initialText,
};
_device.UIHandler.DisplayInputDialog(args, inputText =>
{
Console.WriteLine($"User entered: {inputText}");

View File

@ -561,7 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
context.ResponseData.Write((int)_gyroscopeZeroDriftMode);
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
// Logger.Stub?.PrintStub(LogClass.ServiceHid, new { appletResourceUserId, sixAxisSensorHandle, _gyroscopeZeroDriftMode });
return ResultCode.Success;
}

View File

@ -251,15 +251,16 @@ namespace Ryujinx.Headless.SDL2
[UnmanagedCallersOnly(EntryPoint = "get_current_fps")]
public static unsafe int GetFPS()
{
if (_window != null) {
Switch Device = _window.Device;
if (_window == null || _window.Device == null)
{
return 0;
}
int intValue = (int)Device.Statistics.GetGameFrameRate();
Switch Device = _window.Device;
return intValue;
}
return 0;
int intValue = (int)Device.Statistics.GetGameFrameRate();
return intValue;
}
[UnmanagedCallersOnly(EntryPoint = "initialize")]

View File

@ -486,7 +486,12 @@ namespace Ryujinx.Headless.SDL2
public bool DisplayMessageDialog(string title, string message)
{
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
if (OperatingSystem.IsIOS())
{
AlertHelper.ShowAlert(title, message, false);
} else {
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, title, message, WindowHandle);
}
return true;
}

View File

@ -7,13 +7,16 @@ namespace Ryujinx.Headless.SDL2
{
public static class AlertHelper
{
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
public static extern void showKeyboardAlert(string title, string message, string placeholder);
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
public static extern void showAlert(string title, string message, bool showCancel);
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr getKeyboardInput();
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)]
[DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
private static extern void clearKeyboardInput();
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered)
@ -38,5 +41,10 @@ namespace Ryujinx.Headless.SDL2
}
});
}
public static void ShowAlert(string title, string message, bool cancel) {
showAlert(title, message, cancel);
}
}
}