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 # Update the xcconfig file
sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE" sed -i '' "s/^DOTNET = .*/DOTNET = $ESCAPED_PATH/g" "$XCCONFIG_FILE"
$DOTNET_PATH clean
echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH" echo "Updated MeloNX.xcconfig with DOTNET path: $DOTNET_PATH"

View File

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

View File

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

View File

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

View File

@ -6,40 +6,121 @@
// //
import Foundation import Foundation
import Network
import UIKit
func enableJITEB() { func enableJITEB() {
guard let bundleID = Bundle.main.bundleIdentifier else { if UserDefaults.standard.bool(forKey: "waitForVPN") {
return 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 let task = URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil { if let error = error {
presentAlert(title: "Request Error", message: error.localizedDescription)
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let data = data, let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
let lastWindow = windowScene.windows.last { showLaunchAppAlert(jsonData: data, in: windowScene.windows.last!.rootViewController!)
showLaunchAppAlert(jsonData: data!, in: lastWindow.rootViewController!)
} else { } else {
fatalError("Unable to get Window") fatalError("Unable to get Window")
} }
} }
return
} }
task.resume() 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 { struct LaunchApp: Codable {
let ok: Bool let success: Bool
let error: String? let message: String
let launching: Bool
let position: Int?
let mounting: Bool
} }
func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) { func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
@ -48,28 +129,23 @@ func showLaunchAppAlert(jsonData: Data, in viewController: UIViewController) {
var message = "" var message = ""
if let error = result.error { if !result.success {
message = "Error: \(error)" message += "\n\(result.message)"
} else if result.mounting {
message = "App is mounting..."
} else if result.launching { let alert = UIAlertController(title: "JIT Error", message: message, preferredStyle: .alert)
message = "App is launching..." alert.addAction(UIAlertAction(title: "OK", style: .default))
DispatchQueue.main.async {
viewController.present(alert, animated: true)
}
} else { } else {
message = "App launch status unknown." print("Hopefully JIT is enabled now...")
} Ryujinx.shared.ryuIsJITEnabled()
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)
} }
} catch { } catch {
print(String(data: jsonData, encoding: .utf8))
let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert) let alert = UIAlertController(title: "Decoding Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default)) alert.addAction(UIAlertAction(title: "OK", style: .default))

View File

@ -189,6 +189,24 @@ enum VirtualControllerButton: Int {
case dPadRight case dPadRight
case leftTrigger case leftTrigger
case rightTrigger 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 { enum ThumbstickType: Int {

View File

@ -31,7 +31,7 @@ struct iOSNav<Content: View>: View {
} }
class Ryujinx { class Ryujinx : ObservableObject {
private var isRunning = false private var isRunning = false
let virtualController = VirtualController() let virtualController = VirtualController()
@ -45,6 +45,10 @@ class Ryujinx {
@Published var defMLContentSize: CGFloat? @Published var defMLContentSize: CGFloat?
var thread: Thread!
@Published var jitenabled = false
var shouldMetal: Bool { var shouldMetal: Bool {
metalLayer == nil metalLayer == nil
} }
@ -145,7 +149,7 @@ class Ryujinx {
self.config = config self.config = config
RunLoop.current.perform { [self] in thread = Thread { [self] in
isRunning = true isRunning = true
@ -178,6 +182,10 @@ class Ryujinx {
Self.log("Emulation failed to start: \(error)") Self.log("Emulation failed to start: \(error)")
} }
} }
thread.qualityOfService = .background
thread.name = "MeloNX"
thread.start()
} }
@ -192,6 +200,7 @@ class Ryujinx {
self.metalLayer = nil self.metalLayer = nil
stop_emulation() stop_emulation()
thread.cancel()
} }
var running: Bool { var running: Bool {
@ -500,6 +509,11 @@ class Ryujinx {
static func log(_ message: String) { static func log(_ message: String) {
print("[Ryujinx] \(message)") 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 private let animationDuration: Double = 1.0
@State private var isAnimating = false @State private var isAnimating = false
@State var isLoading = true @State var isLoading = true
@State var jitNotEnabled = false @StateObject var ryujinx = Ryujinx.shared
// MARK: - SDL // MARK: - SDL
var sdlInitFlags: UInt32 = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO 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) _settings = State(initialValue: defaultSettings)
print(SDL_CONTROLLER_BUTTON_LEFTSTICK.rawValue)
initializeSDL() initializeSDL()
} }
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
if game != nil && (!jitNotEnabled || ignoreJIT) { if game != nil && (ryujinx.jitenabled || ignoreJIT) {
gameView gameView
} else if game != nil && jitNotEnabled { } else if game != nil && !ryujinx.jitenabled {
jitErrorView jitErrorView
} else { } else {
mainMenuView mainMenuView
@ -117,9 +119,16 @@ struct ContentView: View {
private var jitErrorView: some View { private var jitErrorView: some View {
Text("") Text("")
.sheet(isPresented: $jitNotEnabled) { .sheet(isPresented:Binding(
get: { !ryujinx.jitenabled },
set: { newValue in
ryujinx.jitenabled = newValue
ryujinx.ryuIsJITEnabled()
})
) {
JITPopover() { JITPopover() {
jitNotEnabled = false ryujinx.jitenabled = false
} }
.interactiveDismissDisabled() .interactiveDismissDisabled()
} }
@ -308,9 +317,9 @@ struct ContentView: View {
} }
private func refreshControllersList() { 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 self.onscreencontroller = onscreen
} }
@ -343,7 +352,7 @@ struct ContentView: View {
} }
do { do {
try Ryujinx.shared.start(with: config) try ryujinx.start(with: config)
} catch { } catch {
print("Error: \(error.localizedDescription)") print("Error: \(error.localizedDescription)")
} }
@ -351,7 +360,8 @@ struct ContentView: View {
private func configureEnvironmentVariables() { private func configureEnvironmentVariables() {
if mVKPreFillBuffer { if mVKPreFillBuffer {
setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1) mVKPreFillBuffer = false
// setenv("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", "2", 1)
} }
if syncqsubmits { if syncqsubmits {
@ -366,8 +376,8 @@ struct ContentView: View {
} }
private func checkJitStatus() { private func checkJitStatus() {
jitNotEnabled = !isJITEnabled() ryujinx.ryuIsJITEnabled()
if jitNotEnabled { if !ryujinx.jitenabled {
if useTrollStore { if useTrollStore {
askForJIT() askForJIT()
} else if jitStreamerEB { } else if jitStreamerEB {
@ -382,9 +392,9 @@ struct ContentView: View {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" { components.host == "game" {
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value { 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 { } 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 import CoreMotion
struct ControllerView: View { 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 { var body: some View {
GeometryReader { geometry in Group {
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad { let isPad = UIDevice.current.userInterfaceIdiom == .pad
VStack {
if isPortrait && !isPad {
Spacer() portraitLayout
VStack { } else {
HStack { landscapeLayout
VStack { }
ShoulderButtonsViewLeft() }
ZStack { .padding()
Joystick() .onChange(of: verticalSizeClass) { _ in
DPadView() updateOrientation()
} }
} .onAppear(perform: updateOrientation)
Spacer() }
VStack {
ShoulderButtonsViewRight() // MARK: - Layouts
ZStack { private var portraitLayout: some View {
Joystick(iscool: true) // hope this works VStack {
ABXYView() Spacer()
} VStack(spacing: 20) {
} HStack(spacing: 30) {
VStack(spacing: 15) {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
} }
}
HStack {
ButtonView(button: .start) // Adding the + button VStack(spacing: 15) {
.padding(.horizontal, 40) ShoulderButtonsViewRight()
ButtonView(button: .back) // Adding the - button ZStack {
.padding(.horizontal, 40) Joystick(iscool: true)
ABXYView()
} }
} }
} }
} else { HStack(spacing: 60) {
// could be landscape HStack {
VStack { ButtonView(button: .leftStick)
.padding()
Spacer() ButtonView(button: .start)
VStack { }
HStack {
HStack {
// gotta fuckin add + and - now ButtonView(button: .back)
VStack { ButtonView(button: .rightStick)
ShoulderButtonsViewLeft() .padding()
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()
}
}
}
} }
// .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 { struct ShoulderButtonsViewLeft: View {
@State var width: CGFloat = 160 @State private var width: CGFloat = 160
@State var height: CGFloat = 20 @State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
HStack { HStack(spacing: 20) {
ButtonView(button: .leftTrigger) ButtonView(button: .leftTrigger)
.padding(.horizontal)
ButtonView(button: .leftShoulder) ButtonView(button: .leftShoulder)
.padding(.horizontal)
} }
.frame(width: width, height: height) .frame(width: width, height: height)
.onAppear() { .onAppear {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2 width *= 1.2
height *= 1.2 height *= 1.2
@ -118,19 +172,17 @@ struct ShoulderButtonsViewLeft: View {
} }
struct ShoulderButtonsViewRight: View { struct ShoulderButtonsViewRight: View {
@State var width: CGFloat = 160 @State private var width: CGFloat = 160
@State var height: CGFloat = 20 @State private var height: CGFloat = 20
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
HStack { HStack(spacing: 20) {
ButtonView(button: .rightShoulder) ButtonView(button: .rightShoulder)
.padding(.horizontal)
ButtonView(button: .rightTrigger) ButtonView(button: .rightTrigger)
.padding(.horizontal)
} }
.frame(width: width, height: height) .frame(width: width, height: height)
.onAppear() { .onAppear {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
width *= 1.2 width *= 1.2
height *= 1.2 height *= 1.2
@ -143,21 +195,21 @@ struct ShoulderButtonsViewRight: View {
} }
struct DPadView: View { struct DPadView: View {
@State var size: CGFloat = 145 @State private var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
VStack { VStack(spacing: 5) {
ButtonView(button: .dPadUp) ButtonView(button: .dPadUp)
HStack { HStack(spacing: 20) {
ButtonView(button: .dPadLeft) ButtonView(button: .dPadLeft)
Spacer(minLength: 20) Spacer(minLength: 20)
ButtonView(button: .dPadRight) ButtonView(button: .dPadRight)
} }
ButtonView(button: .dPadDown) ButtonView(button: .dPadDown)
.padding(.horizontal)
} }
.frame(width: size, height: size) .frame(width: size, height: size)
.onAppear() { .onAppear {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2 size *= 1.2
} }
@ -168,22 +220,21 @@ struct DPadView: View {
} }
struct ABXYView: View { struct ABXYView: View {
@State var size: CGFloat = 145 @State private var size: CGFloat = 145
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var body: some View { var body: some View {
VStack { VStack(spacing: 5) {
ButtonView(button: .X) ButtonView(button: .X)
HStack { HStack(spacing: 20) {
ButtonView(button: .Y) ButtonView(button: .Y)
Spacer(minLength: 20) Spacer(minLength: 20)
ButtonView(button: .A) ButtonView(button: .A)
} }
ButtonView(button: .B) ButtonView(button: .B)
.padding(.horizontal)
} }
.frame(width: size, height: size) .frame(width: size, height: size)
.onAppear() { .onAppear {
if UIDevice.current.systemName.contains("iPadOS") { if UIDevice.current.systemName.contains("iPadOS") {
size *= 1.2 size *= 1.2
} }
@ -195,58 +246,90 @@ struct ABXYView: View {
struct ButtonView: View { struct ButtonView: View {
var button: VirtualControllerButton var button: VirtualControllerButton
@State var width: CGFloat = 45 @State private var width: CGFloat = 45
@State var height: CGFloat = 45 @State private var height: CGFloat = 45
@State var isPressed = false @State private var isPressed = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State private var debounceTimer: Timer?
var body: some View { var body: some View {
Image(systemName: buttonText) Image(systemName: buttonText)
.resizable() .resizable()
.scaledToFit()
.frame(width: width, height: height) .frame(width: width, height: height)
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray) .foregroundColor(true ? Color.white.opacity(0.9) : Color.black.opacity(0.9))
.opacity(isPressed ? 0.4 : 0.7) .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( .gesture(
DragGesture(minimumDistance: 0) DragGesture(minimumDistance: 0)
.onChanged { _ in .onChanged { _ in
if !self.isPressed { handleButtonPress()
self.isPressed = true
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.heavy)
}
} }
.onEnded { _ in .onEnded { _ in
self.isPressed = false handleButtonRelease()
Ryujinx.shared.virtualController.setButtonState(0, for: button)
} }
) )
.onAppear() { .onAppear {
if button == .leftTrigger || button == .rightTrigger || button == .leftShoulder || button == .rightShoulder { configureSizeForButton()
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)
} }
} }
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 { private var buttonText: String {
switch button { switch button {
@ -258,6 +341,10 @@ struct ButtonView: View {
return "x.circle.fill" return "x.circle.fill"
case .Y: case .Y:
return "y.circle.fill" return "y.circle.fill"
case .leftStick:
return "l.joystick.press.down.fill"
case .rightStick:
return "r.joystick.press.down.fill"
case .dPadUp: case .dPadUp:
return "arrowtriangle.up.circle.fill" return "arrowtriangle.up.circle.fill"
case .dPadDown: case .dPadDown:
@ -267,7 +354,7 @@ struct ButtonView: View {
case .dPadRight: case .dPadRight:
return "arrowtriangle.right.circle.fill" return "arrowtriangle.right.circle.fill"
case .leftTrigger: case .leftTrigger:
return"zl.rectangle.roundedtop.fill" return "zl.rectangle.roundedtop.fill"
case .rightTrigger: case .rightTrigger:
return "zr.rectangle.roundedtop.fill" return "zr.rectangle.roundedtop.fill"
case .leftShoulder: case .leftShoulder:
@ -275,16 +362,11 @@ struct ButtonView: View {
case .rightShoulder: case .rightShoulder:
return "r.rectangle.roundedbottom.fill" return "r.rectangle.roundedbottom.fill"
case .start: case .start:
return "plus.circle.fill" // System symbol for + return "plus.circle.fill"
case .back: case .back:
return "minus.circle.fill" // System symbol for - return "minus.circle.fill"
case .guide: case .guide:
return "house.circle.fill" return "house.circle.fill"
// This should be all the cases
default:
return ""
} }
} }
} }

View File

@ -11,7 +11,7 @@ import SwiftUIJoystick
public struct Joystick: View { public struct Joystick: View {
@State var iscool: Bool? = nil @State var iscool: Bool? = nil
@Environment(\.colorScheme) var colorScheme
@ObservedObject public var joystickMonitor = JoystickMonitor() @ObservedObject public var joystickMonitor = JoystickMonitor()
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
var dragDiameter: CGFloat { var dragDiameter: CGFloat {
@ -36,8 +36,13 @@ public struct Joystick: View {
.hidden() .hidden()
}, },
foreground: { foreground: {
Circle().fill(Color.gray) Circle()
.opacity(0.7) .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) locksInPlace: false)
.onChange(of: self.joystickMonitor.xyPoint) { newValue in .onChange(of: self.joystickMonitor.xyPoint) { newValue in

View File

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

View File

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

View File

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

View File

@ -48,11 +48,15 @@ struct SettingsView: View {
@AppStorage("showlogsgame") var showlogsgame: Bool = false @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 showResolutionInfo = false
@State private var showAnisotropicInfo = false @State private var showAnisotropicInfo = false
@State private var showControllerInfo = false @State private var showControllerInfo = false
@State private var searchText = "" @State private var searchText = ""
@AppStorage("portal") var gamepo = false @AppStorage("portal") var gamepo = false
@StateObject var ryujinx = Ryujinx.shared
var filteredMemoryModes: [(String, String)] { var filteredMemoryModes: [(String, String)] {
guard !searchText.isEmpty else { return memoryManagerModes } guard !searchText.isEmpty else { return memoryManagerModes }
@ -286,6 +290,11 @@ struct SettingsView: View {
}.tint(.blue) }.tint(.blue)
Toggle(isOn: $stickButton) {
labelWithIcon("Show Stick Buttons", iconName: "l.joystick.press.down")
}.tint(.blue)
Toggle(isOn: $ryuDemo) { Toggle(isOn: $ryuDemo) {
labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw") labelWithIcon("On-Screen Controller (Demo)", iconName: "hand.draw")
} }
@ -451,10 +460,22 @@ struct SettingsView: View {
} }
.tint(.blue) .tint(.blue)
.contextMenu { .contextMenu {
Button {
waitForVPN.toggle()
} label: {
Label {
Text("Wait for VPN")
} icon: {
if waitForVPN {
Image(systemName: "checkmark")
}
}
}
Button { Button {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let mainWindow = windowScene.windows.last { 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 let learnMoreButton = UIAlertAction(title: "Learn More", style: .default) {_ in
UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!) UIApplication.shared.open(URL(string: "https://jkcoxson.com/jitstreamer")!)
@ -539,7 +560,11 @@ struct SettingsView: View {
model.hasPrefix("iPhone") ? "iphone" : model.hasPrefix("iPhone") ? "iphone" :
"macwindow" "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") 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 { Section {
DisclosureGroup { DisclosureGroup {
Toggle(isOn: $mVKPreFillBuffer) {
labelWithIcon("MVK: Pre-Fill Metal Command Buffers", iconName: "gearshape")
}.tint(.blue)
Toggle(isOn: $config.dfsIntegrityChecks) { Toggle(isOn: $config.dfsIntegrityChecks) {
labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield") labelWithIcon("Disable FS Integrity Checks", iconName: "checkmark.shield")
}.tint(.blue) }.tint(.blue)
@ -637,6 +658,8 @@ struct SettingsView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.onAppear { .onAppear {
mVKPreFillBuffer = false
if let configs = loadSettings() { if let configs = loadSettings() {
self.config = configs self.config = configs
} else { } else {

View File

@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
import UniformTypeIdentifiers import UniformTypeIdentifiers
// MARK: - Models
struct DownloadableContentNca: Codable, Hashable { struct DownloadableContentNca: Codable, Hashable {
var fullPath: String var fullPath: String
var titleId: UInt 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 containerPath: String
var downloadableContentNcaList: [DownloadableContentNca] var downloadableContentNcaList: [DownloadableContentNca]
var filename: String {
(containerPath as NSString).lastPathComponent
}
var isEnabled: Bool {
downloadableContentNcaList.first?.enabled == true
}
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case containerPath = "path" case containerPath = "path"
@ -30,136 +40,265 @@ struct DownloadableContentContainer: Codable, Hashable {
} }
} }
// MARK: - View
struct DLCManagerSheet: View { struct DLCManagerSheet: View {
// MARK: - Properties
@Binding var game: Game! @Binding var game: Game!
@State private var isSelectingGameDLC = false @State private var isSelectingGameDLC = false
@State private var dlcs: [DownloadableContentContainer] = [] @State private var dlcs: [DownloadableContentContainer] = []
@Environment(\.dismiss) private var dismiss
// MARK: - Body
var body: some View { var body: some View {
NavigationView { iOSNav {
let withIndex = dlcs.enumerated().map { $0 } List {
List(withIndex, id: \.element.containerPath) { index, dlc in if dlcs.isEmpty {
Button(action: { emptyStateView
let toggle = dlcs[index].downloadableContentNcaList.first?.enabled ?? true } else {
dlcs[index].downloadableContentNcaList.mutableForEach { $0.enabled = !toggle } ForEach(dlcs) { dlc in
Self.saveDlcs(game, dlc: dlcs) dlcRow(dlc)
}) {
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")
} }
.onDelete(perform: removeDLCs)
} }
} }
.navigationTitle("\(game.titleName) DLCs") .navigationTitle("\(game.titleName) DLCs")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
Button("Add", systemImage: "plus") { ToolbarItem(placement: .navigationBarLeading) {
isSelectingGameDLC = true Button("Done") {
dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isSelectingGameDLC = true
} label: {
Label("Add DLC", systemImage: "plus")
}
} }
} }
.onAppear {
loadData()
}
} }
.onAppear { .fileImporter(
dlcs = Self.loadDlc(game) isPresented: $isSelectingGameDLC,
} allowedContentTypes: [.item],
.fileImporter(isPresented: $isSelectingGameDLC, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in allowsMultipleSelection: true,
switch result { onCompletion: handleFileImport
case .success(let urls): )
for url in urls { }
guard url.startAccessingSecurityScopedResource() else {
print("Failed to access security-scoped resource") // MARK: - Views
return private var emptyStateView: some View {
} Group {
defer { url.stopAccessingSecurityScopedResource() } if #available(iOS 17, *) {
ContentUnavailableView(
do { "No DLCs Found",
let fileManager = FileManager.default systemImage: "puzzlepiece.extension",
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! description: Text("Tap the + button to add game DLCs.")
let dlcDirectory = documentsDirectory.appendingPathComponent("dlc") )
let romDlcDirectory = dlcDirectory.appendingPathComponent(game.titleId) } else {
VStack(spacing: 20) {
if !fileManager.fileExists(atPath: dlcDirectory.path) { Spacer()
try fileManager.createDirectory(at: dlcDirectory, withIntermediateDirectories: true, attributes: nil)
} Image(systemName: "puzzlepiece.extension")
.font(.system(size: 64))
if !fileManager.fileExists(atPath: romDlcDirectory.path) { .foregroundColor(.secondary)
try fileManager.createDirectory(at: romDlcDirectory, withIntermediateDirectories: true, attributes: nil)
} Text("No DLCs Found")
.font(.title2)
let dlcContent = Ryujinx.shared.getDlcNcaList(titleId: game.titleId, path: url.path) .fontWeight(.semibold)
guard !dlcContent.isEmpty else { return }
Text("Tap the + button to add game DLCs.")
let destinationURL = romDlcDirectory.appendingPathComponent(url.lastPathComponent) .font(.subheadline)
try? fileManager.copyItem(at: url, to: destinationURL) .foregroundColor(.secondary)
.multilineTextAlignment(.center)
let container = DownloadableContentContainer( .padding(.horizontal)
containerPath: Self.relativeDlcDirectoryPath(for: game, dlcPath: destinationURL),
downloadableContentNcaList: dlcContent Spacer()
)
dlcs.append(container)
Self.saveDlcs(game, dlc: dlcs)
} catch {
print("Error copying game file: \(error)")
}
} }
case .failure(let err): .frame(maxWidth: .infinity)
print("File import failed: \(err.localizedDescription)") .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 { private extension DLCManagerSheet {
static func loadDlc(_ game: Game) -> [DownloadableContentContainer] { static func loadDlc(_ game: Game) -> [DownloadableContentContainer] {
let jsonURL = dlcJsonPath(for: game) let jsonURL = dlcJsonPath(for: game)
try? FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
guard let data = try? Data(contentsOf: jsonURL), do {
var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data) try FileManager.default.createDirectory(at: jsonURL.deletingLastPathComponent(), withIntermediateDirectories: true)
else { return [] }
guard FileManager.default.fileExists(atPath: jsonURL.path),
result = result.filter { container in let data = try? Data(contentsOf: jsonURL),
let path = URL.documentsDirectory.appendingPathComponent(container.containerPath) var result = try? JSONDecoder().decode([DownloadableContentContainer].self, from: data)
return FileManager.default.fileExists(atPath: path.path) 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]) { static func saveDlcs(_ game: Game, dlc: [DownloadableContentContainer]) {
guard let data = try? JSONEncoder().encode(dlc) else { return } do {
try? data.write(to: dlcJsonPath(for: game)) 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 { static func relativeDlcDirectoryPath(for game: Game, dlcPath: URL) -> String {
"dlc/\(game.titleId)/\(dlcPath.lastPathComponent)" "dlc/\(game.titleId)/\(dlcPath.lastPathComponent)"
} }
static func dlcJsonPath(for game: Game) -> URL { 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 { extension URL {
@available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above") @available(iOS, introduced: 15.0, deprecated: 16.0, message: "Use URL.documentsDirectory on iOS 16 and above")
static var documentsDirectory: URL { static var documentsDirectory: URL {

View File

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

View File

@ -21,7 +21,7 @@ struct SetupView: View {
var body: some View { var body: some View {
iOSNav { iOSNav {
ZStack { ZStack {
if UIDevice.current.userInterfaceIdiom == .pad { if UIDevice.current.systemName.contains("iPadOS") {
iPadSetupView( iPadSetupView(
finished: $finished, finished: $finished,
isImportingKeys: $isImportingKeys, isImportingKeys: $isImportingKeys,
@ -65,8 +65,9 @@ struct SetupView: View {
initialize() initialize()
finished = false finished = false
keysImported = Ryujinx.shared.checkIfKeysImported() 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) Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
print(Double(Ryujinx.shared.fetchFirmwareVersion()) ?? 0) let firmware = Ryujinx.shared.fetchFirmwareVersion()
firmImported = (firmware == "" ? "0" : firmware) != "0"
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
alertMessage = "Firmware installed successfully" alertMessage = "Firmware installed successfully"
showAlert = true 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> <dict>
<key>files</key> <key>files</key>
<dict> <dict>
<key>Headers/RyujinxKeyboard.h</key> <key>Headers/RyujinxHelper.h</key>
<data> <data>
5P7GN4g050n199pV6/+SpfMBgJc= 5P7GN4g050n199pV6/+SpfMBgJc=
</data> </data>
<key>Info.plist</key> <key>Info.plist</key>
<data> <data>
hYdI/ktAKwjBSfaJpt6Yc8UKLCY= UOH9NuuEcz5NQiQlrM2LNFaG2pI=
</data> </data>
<key>Modules/module.modulemap</key> <key>Modules/module.modulemap</key>
<data> <data>
0kFAMoTn+4Q1J/dM6uMLe3EhbL0= JDij7psMD6pZZpigUfkSQldib+I=
</data> </data>
</dict> </dict>
<key>files2</key> <key>files2</key>
<dict> <dict>
<key>Headers/RyujinxKeyboard.h</key> <key>Headers/RyujinxHelper.h</key>
<dict> <dict>
<key>hash2</key> <key>hash2</key>
<data> <data>
@ -30,7 +30,7 @@
<dict> <dict>
<key>hash2</key> <key>hash2</key>
<data> <data>
K+ZyxKhTI4bMVZuHBIspvd2PFqvCOlVUFYmwF96O5NQ= 5t/lQcpkzC5bwJqFQqIf6h1ldlhHouYzDawRVrnUeyM=
</data> </data>
</dict> </dict>
</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) 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); int bufferDataLength = GetBufferDataLength(data.Length);
@ -786,21 +786,39 @@ namespace Ryujinx.Graphics.Vulkan
for (int i = 0; i < layers; i++) 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) if (offset >= data.Length)
break; 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) else if (region.HasValue)
{ {
var rect = region.Value; var rect = region.Value;
if (rect.Width <= 0 || rect.Height <= 0)
return;
int dataPerPixel = data.Length / (rect.Width * rect.Height); int dataPerPixel = data.Length / (rect.Width * rect.Height);
if (dataPerPixel <= 0)
return;
int rowStride = rect.Width * dataPerPixel; int rowStride = rect.Width * dataPerPixel;
int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride); int rowsPerChunk = Math.Max(1, MaxChunkSize / rowStride);
@ -811,42 +829,63 @@ namespace Ryujinx.Graphics.Vulkan
while (currentY < rect.Y + originalHeight) while (currentY < rect.Y + originalHeight)
{ {
int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY); int chunkHeight = Math.Min(rowsPerChunk, rect.Y + originalHeight - currentY);
if (chunkHeight <= 0)
break;
var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight); var chunkRegion = new Rectangle<int>(rect.X, currentY, rect.Width, chunkHeight);
int chunkSize = chunkHeight * rowStride; 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) if (offset >= data.Length)
break; 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 else
{ {
ProcessChunk(data, layer, level, layers, levels, singleSlice, region); 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); if (loadInline)
{
using (var imageAuto = GetImage()) _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); CopyDataToBuffer(bufferHolder.GetDataStorage(0, chunkBufferLength), chunkData);
var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value; var buffer = bufferHolder.GetBuffer(cbs.CommandBuffer).Get(cbs).Value;
@ -854,6 +893,11 @@ namespace Ryujinx.Graphics.Vulkan
if (chunkRegion.HasValue) if (chunkRegion.HasValue)
{ {
var region = chunkRegion.Value;
if (region.Width <= 0 || region.Height <= 0)
return;
CopyFromOrToBuffer( CopyFromOrToBuffer(
cbs.CommandBuffer, cbs.CommandBuffer,
buffer, buffer,
@ -862,10 +906,10 @@ namespace Ryujinx.Graphics.Vulkan
false, false,
chunkLayer, chunkLayer,
chunkLevel, chunkLevel,
chunkRegion.Value.X, region.X,
chunkRegion.Value.Y, region.Y,
chunkRegion.Value.Width, region.Width,
chunkRegion.Value.Height); region.Height);
} }
else else
{ {
@ -881,7 +925,11 @@ namespace Ryujinx.Graphics.Vulkan
chunkLevels, chunkLevels,
chunkSingleSlice); chunkSingleSlice);
} }
} }
catch (Exception e)
{
}
} }
} }

View File

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

View File

@ -561,7 +561,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
context.ResponseData.Write((int)_gyroscopeZeroDriftMode); 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; return ResultCode.Success;
} }

View File

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

View File

@ -486,7 +486,12 @@ namespace Ryujinx.Headless.SDL2
public bool DisplayMessageDialog(string title, string message) 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; return true;
} }

View File

@ -7,13 +7,16 @@ namespace Ryujinx.Headless.SDL2
{ {
public static class AlertHelper 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); 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(); private static extern IntPtr getKeyboardInput();
[DllImport("RyujinxKeyboard.framework/RyujinxKeyboard", CallingConvention = CallingConvention.Cdecl)] [DllImport("RyujinxHelper.framework/RyujinxHelper", CallingConvention = CallingConvention.Cdecl)]
private static extern void clearKeyboardInput(); private static extern void clearKeyboardInput();
public static void ShowAlertWithTextInput(string title, string message, string placeholder, Action<string> onTextEntered) 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);
}
} }
} }