App Icon Switcher, Per-App Settings Page, Update Virtual Controller and more

This commit is contained in:
Stossy11 2025-06-13 07:03:22 +10:00
parent fad5915c2e
commit c21dd01a58
51 changed files with 2079 additions and 364 deletions

View file

@ -75,7 +75,7 @@ namespace ARMeilleure.Translation
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit); FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
Stubs = new TranslatorStubs(FunctionTable); Stubs = new TranslatorStubs(FunctionTable);
FunctionTable.Fill = (ulong)Stubs.DispatchStub; FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
} }
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled) public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)

View file

@ -8,4 +8,4 @@
// 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.7.0 VERSION = 2.0

View file

@ -32,13 +32,6 @@
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
4E59B0A32DEA5CA9004BFF2A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
};
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = { 4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */; containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@ -53,6 +46,13 @@
remoteGlobalIDString = 4E80A98C2CD6F54500029585; remoteGlobalIDString = 4E80A98C2CD6F54500029585;
remoteInfo = MeloNX; remoteInfo = MeloNX;
}; };
4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
proxyType = 1;
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
};
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = { BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 4E80A9852CD6F54500029585 /* Project object */; containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
@ -294,7 +294,7 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
4E59B0A42DEA5CA9004BFF2A /* PBXTargetDependency */, 4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
4E80A98F2CD6F54500029585 /* MeloNX */, 4E80A98F2CD6F54500029585 /* MeloNX */,
@ -482,11 +482,6 @@
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
4E59B0A42DEA5CA9004BFF2A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4E59B0A32DEA5CA9004BFF2A /* PBXContainerItemProxy */;
};
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = { 4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 4E80A98C2CD6F54500029585 /* MeloNX */; target = 4E80A98C2CD6F54500029585 /* MeloNX */;
@ -497,6 +492,11 @@
target = 4E80A98C2CD6F54500029585 /* MeloNX */; target = 4E80A98C2CD6F54500029585 /* MeloNX */;
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */; targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
}; };
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
targetProxy = 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */;
};
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = { BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */; target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
@ -647,13 +647,16 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements; CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = 95J8WZ4TN8;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = NO; ENABLE_TESTABILITY = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -773,6 +776,18 @@
"$(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",
); );
GCC_OPTIMIZATION_LEVEL = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1008,6 +1023,22 @@
"$(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",
"$(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;
@ -1025,13 +1056,16 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */; baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements; CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8; DEVELOPMENT_TEAM = 95J8WZ4TN8;
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -1151,6 +1185,18 @@
"$(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",
); );
GCC_OPTIMIZATION_LEVEL = z; GCC_OPTIMIZATION_LEVEL = z;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1386,6 +1432,22 @@
"$(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",
"$(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;

View file

@ -59,6 +59,8 @@ void initialize();
int main_ryujinx_sdl(int argc, char **argv); int main_ryujinx_sdl(int argc, char **argv);
int update_settings_external(int argc, char **argv);
int get_current_fps(); int get_current_fps();
void touch_began(float x, float y, int index); void touch_began(float x, float y, int index);

View file

@ -9,8 +9,6 @@ import Foundation
import Network import Network
import UIKit import UIKit
func stikJITorStikDebug() -> Int { func stikJITorStikDebug() -> Int {
let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil) let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
@ -25,15 +23,28 @@ func stikJITorStikDebug() -> Int {
return 0 // Not Found return 0 // Not Found
} }
func checkforOld() -> Bool {
let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
if checkifappinstalled(changeAppUI("Y29tLnN0b3NzeTExLlBvbWVsbw==") ?? "") {
return true
}
if checkifappinstalled(changeAppUI("Y29tLnN0b3NzeTExLlBvbWVsbw==") ?? "" + ".\(String(teamid ?? ""))") {
return true
}
if checkifappinstalled((Bundle.main.bundleIdentifier ?? "").replacingOccurrences(of: "MeloNX", with: changeAppUI("UG9tZWxv") ?? "")) {
return true
}
return false
}
func checkifappinstalled(_ id: String) -> Bool { func checkifappinstalled(_ id: String) -> Bool {
guard let handle = dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices", RTLD_LAZY) else { guard let handle = dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices", RTLD_LAZY) else {
if let error = dlerror() {
print(String(cString: error))
}
return false return false
// fatalError("Failed to open dylib")
} }
typealias SBSLaunchApplicationWithIdentifierFunc = @convention(c) (CFString, Bool) -> Int32 typealias SBSLaunchApplicationWithIdentifierFunc = @convention(c) (CFString, Bool) -> Int32

View file

@ -356,7 +356,9 @@ class Ryujinx : ObservableObject {
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs) let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
if result != 0 { if result != 0 {
self.isRunning = false DispatchQueue.main.async {
self.isRunning = false
}
if let accessing, accessing { if let accessing, accessing {
url!.stopAccessingSecurityScopedResource() url!.stopAccessingSecurityScopedResource()
} }
@ -365,7 +367,9 @@ class Ryujinx : ObservableObject {
} }
} }
} catch { } catch {
self.isRunning = false DispatchQueue.main.async {
self.isRunning = false
}
Thread.sleep(forTimeInterval: 0.3) Thread.sleep(forTimeInterval: 0.3)
let logs = LogCapture.shared.capturedLogs let logs = LogCapture.shared.capturedLogs
let parsedLogs = extractExceptionInfo(logs) let parsedLogs = extractExceptionInfo(logs)
@ -384,14 +388,19 @@ class Ryujinx : ObservableObject {
presentAlert(title: "MeloNX Crashed!", message: parsedLogs.exceptionType + ": " + parsedLogs.message) { presentAlert(title: "MeloNX Crashed!", message: parsedLogs.exceptionType + ": " + parsedLogs.message) {
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
assert(true, parsedLogs.exceptionType) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
exit(0)
}
} }
} }
} else { } else {
DispatchQueue.main.async { DispatchQueue.main.async {
presentAlert(title: "MeloNX Crashed!", message: "Unknown Error") { presentAlert(title: "MeloNX Crashed!", message: "Unknown Error") {
assert(true, "Exception was not detected") UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
exit(0)
}
} }
} }
} }
@ -517,7 +526,7 @@ class Ryujinx : ObservableObject {
} }
} }
private func buildCommandLineArgs(from config: Arguments) -> [String] { func buildCommandLineArgs(from config: Arguments) -> [String] {
var args: [String] = [] var args: [String] = []
// Add the game path // Add the game path
@ -648,11 +657,10 @@ class Ryujinx : ObservableObject {
// Append the input dsu servers (limit to 8 (used to be 4) just in case) // Append the input dsu servers (limit to 8 (used to be 4) just in case)
if !config.inputDSUServers.isEmpty { if !config.inputDSUServers.isEmpty {
config.inputDSUServers.prefix(8).enumerated().forEach { index, inputDSUServer in config.inputDSUServers.prefix(8).enumerated().forEach { index, inputDSUServer in
if config.handHeldController { if index == 0 {
args.append(contentsOf: ["\(index == 0 ? "--input-dsu-server-handheld" : "--input-dsu-server-\(index + 1)")", inputDSUServer]) args.append(contentsOf: ["--input-dsu-server-handheld", inputDSUServer])
} else {
args.append(contentsOf: ["--input-dsu-server-\(index + 1)", inputDSUServer])
} }
args.append(contentsOf: ["--input-dsu-server-\(index + 1)", inputDSUServer])
} }
} }

View file

@ -264,6 +264,7 @@ struct ABXYView: View {
struct ButtonView: View { struct ButtonView: View {
var button: VirtualControllerButton var button: VirtualControllerButton
var callback: (() -> Void)? = nil
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false @AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0 @AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@ -344,23 +345,29 @@ struct ButtonView: View {
} }
private func handleButtonPress() { private func handleButtonPress() {
guard !isPressed || istoggle else { return } if let callback {
callback()
if istoggle {
toggleState.toggle()
isPressed = toggleState
let value = toggleState ? 1 : 0
Ryujinx.shared.virtualController.setButtonState(Uint8(value), for: button)
Haptics.shared.play(.medium)
} else { } else {
isPressed = true guard !isPressed || istoggle else { return }
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.medium) if istoggle {
toggleState.toggle()
isPressed = toggleState
let value = toggleState ? 1 : 0
Ryujinx.shared.virtualController.setButtonState(Uint8(value), for: button)
Haptics.shared.play(.medium)
} else {
isPressed = true
Ryujinx.shared.virtualController.setButtonState(1, for: button)
Haptics.shared.play(.medium)
}
} }
} }
private func handleButtonRelease() { private func handleButtonRelease() {
if istoggle { return } if istoggle { return }
if let callback { return }
guard isPressed else { return } guard isPressed else { return }
@ -397,40 +404,23 @@ struct ButtonView: View {
// Centralized button configuration // Centralized button configuration
private var buttonConfig: ButtonConfiguration { private var buttonConfig: ButtonConfiguration {
switch button { switch button {
case .A: case .A: return .init(iconName: "a.circle.fill")
return ButtonConfiguration(iconName: "a.circle.fill") case .B: return .init(iconName: "b.circle.fill")
case .B: case .X: return .init(iconName: "x.circle.fill")
return ButtonConfiguration(iconName: "b.circle.fill") case .Y: return .init(iconName: "y.circle.fill")
case .X: case .leftStick: return .init(iconName: "l.joystick.press.down.fill")
return ButtonConfiguration(iconName: "x.circle.fill") case .rightStick: return .init(iconName: "r.joystick.press.down.fill")
case .Y: case .dPadUp: return .init(iconName: "arrowtriangle.up.circle.fill")
return ButtonConfiguration(iconName: "y.circle.fill") case .dPadDown: return .init(iconName: "arrowtriangle.down.circle.fill")
case .leftStick: case .dPadLeft: return .init(iconName: "arrowtriangle.left.circle.fill")
return ButtonConfiguration(iconName: "l.joystick.press.down.fill") case .dPadRight: return .init(iconName: "arrowtriangle.right.circle.fill")
case .rightStick: case .leftTrigger: return .init(iconName: "zl.rectangle.roundedtop.fill")
return ButtonConfiguration(iconName: "r.joystick.press.down.fill") case .rightTrigger: return .init(iconName: "zr.rectangle.roundedtop.fill")
case .dPadUp: case .leftShoulder: return .init(iconName: "l.rectangle.roundedbottom.fill")
return ButtonConfiguration(iconName: "arrowtriangle.up.circle.fill") case .rightShoulder: return .init(iconName: "r.rectangle.roundedbottom.fill")
case .dPadDown: case .start: return .init(iconName: "plus.circle.fill")
return ButtonConfiguration(iconName: "arrowtriangle.down.circle.fill") case .back: return .init(iconName: "minus.circle.fill")
case .dPadLeft: case .guide: return .init(iconName: "gearshape.fill")
return ButtonConfiguration(iconName: "arrowtriangle.left.circle.fill")
case .dPadRight:
return ButtonConfiguration(iconName: "arrowtriangle.right.circle.fill")
case .leftTrigger:
return ButtonConfiguration(iconName: "zl.rectangle.roundedtop.fill")
case .rightTrigger:
return ButtonConfiguration(iconName: "zr.rectangle.roundedtop.fill")
case .leftShoulder:
return ButtonConfiguration(iconName: "l.rectangle.roundedbottom.fill")
case .rightShoulder:
return ButtonConfiguration(iconName: "r.rectangle.roundedbottom.fill")
case .start:
return ButtonConfiguration(iconName: "plus.circle.fill")
case .back:
return ButtonConfiguration(iconName: "minus.circle.fill")
case .guide:
return ButtonConfiguration(iconName: "house.circle.fill")
} }
} }
@ -438,3 +428,121 @@ struct ButtonView: View {
let iconName: String let iconName: String
} }
} }
struct ExtButtonIconView: View {
var button: VirtualControllerButton
var opacity = 0.8
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
@State private var size: CGSize = .zero
var body: some View {
Circle()
.foregroundStyle(.clear.opacity(0))
.overlay {
Image(systemName: buttonConfig.iconName)
.resizable()
.scaledToFit()
.frame(width: size.width / 1.5, height: size.height / 1.5)
.foregroundStyle(.white)
.opacity(opacity)
.allowsHitTesting(false)
}
.frame(width: size.width, height: size.height)
.background(
buttonBackground
)
.onAppear {
size = calculateButtonSize()
}
.onChange(of: controllerScale) { _ in
size = calculateButtonSize()
}
}
private var buttonBackground: some View {
Group {
if !button.isTrigger && button != .leftStick && button != .rightStick {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: size.width * 1.25, height: size.height * 1.25)
} else if button == .leftStick || button == .rightStick {
Image(systemName: buttonConfig.iconName)
.resizable()
.scaledToFit()
.frame(width: size.width * 1.25, height: size.height * 1.25)
.foregroundColor(Color.gray.opacity(0.4))
} else if button.isTrigger {
Image(systemName: convertTriggerIconToButton(buttonConfig.iconName))
.resizable()
.scaledToFit()
.frame(width: size.width * 1.25, height: size.height * 1.25)
.foregroundColor(Color.gray.opacity(0.4))
}
}
}
private func convertTriggerIconToButton(_ iconName: String) -> String {
var converted = iconName
if iconName.hasPrefix("zl") || iconName.hasPrefix("zr") {
converted = String(iconName.dropFirst(3))
} else {
converted = String(iconName.dropFirst(2))
}
converted = converted
.replacingOccurrences(of: "rectangle", with: "button")
.replacingOccurrences(of: ".fill", with: ".horizontal.fill")
return converted
}
private func calculateButtonSize() -> CGSize {
let baseWidth: CGFloat
let baseHeight: CGFloat
if button.isTrigger {
baseWidth = 70
baseHeight = 40
} else if button.isSmall {
baseWidth = 35
baseHeight = 35
} else {
baseWidth = 45
baseHeight = 45
}
let deviceMultiplier = UIDevice.current.userInterfaceIdiom == .pad ? 1.2 : 1.0
let scaleMultiplier = CGFloat(controllerScale)
return CGSize(
width: baseWidth * deviceMultiplier * scaleMultiplier,
height: baseHeight * deviceMultiplier * scaleMultiplier
)
}
private var buttonConfig: ButtonConfiguration {
switch button {
case .A: return .init(iconName: "a.circle.fill")
case .B: return .init(iconName: "b.circle.fill")
case .X: return .init(iconName: "x.circle.fill")
case .Y: return .init(iconName: "y.circle.fill")
case .leftStick: return .init(iconName: "l.joystick.press.down.fill")
case .rightStick: return .init(iconName: "r.joystick.press.down.fill")
case .dPadUp: return .init(iconName: "arrowtriangle.up.circle.fill")
case .dPadDown: return .init(iconName: "arrowtriangle.down.circle.fill")
case .dPadLeft: return .init(iconName: "arrowtriangle.left.circle.fill")
case .dPadRight: return .init(iconName: "arrowtriangle.right.circle.fill")
case .leftTrigger: return .init(iconName: "zl.rectangle.roundedtop.fill")
case .rightTrigger: return .init(iconName: "zr.rectangle.roundedtop.fill")
case .leftShoulder: return .init(iconName: "l.rectangle.roundedbottom.fill")
case .rightShoulder: return .init(iconName: "r.rectangle.roundedbottom.fill")
case .start: return .init(iconName: "plus.circle.fill")
case .back: return .init(iconName: "minus.circle.fill")
case .guide: return .init(iconName: "gearshape.fill")
}
}
struct ButtonConfiguration {
let iconName: String
}
}

View file

@ -18,7 +18,7 @@ struct Joystick: View {
@State private var offset: CGSize = .zero @State private var offset: CGSize = .zero
@Binding var showBackground: Bool @Binding var showBackground: Bool
let sensitivity: CGFloat = 1.5 let sensitivity: CGFloat = 1.2
var dragGesture: some Gesture { var dragGesture: some Gesture {

View file

@ -28,7 +28,6 @@ struct JoystickController: View {
VStack { VStack {
Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground) Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground)
.onChange(of: position) { newValue in .onChange(of: position) { newValue in
if iscool != nil { if iscool != nil {
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y) Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
} else { } else {

View file

@ -24,6 +24,8 @@ struct EmulationView: View {
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@State private var isInBackground = false @State private var isInBackground = false
@State var showSettings = false
@State var pauseEmu = true
@AppStorage("location-enabled") var locationenabled: Bool = false @AppStorage("location-enabled") var locationenabled: Bool = false
var body: some View { var body: some View {
@ -80,15 +82,47 @@ struct EmulationView: View {
if ssb { if ssb {
HStack { HStack {
Image(systemName: "arrow.left.circle") Menu {
.resizable()
.frame(width: 50, height: 50) /*
.onTapGesture { Button {
showSettings.toggle()
} label: {
Label {
Text("Game Settings")
} icon: {
Image(systemName: "gearshape.circle")
}
}
*/
Button {
pause_emulation(pauseEmu)
pauseEmu.toggle()
} label: {
Label {
Text(pauseEmu ? "Pause" : "Play")
} icon: {
Image(systemName: pauseEmu ? "pause.circle" : "play.circle")
}
}
Button(role: .destructive) {
startgame = nil startgame = nil
stop_emulation() stop_emulation()
try? Ryujinx.shared.stop() try? Ryujinx.shared.stop()
} label: {
Label {
Text("Exit (Unstable)")
} icon: {
Image(systemName: "x.circle")
}
} }
.padding() } label: {
ExtButtonIconView(button: .guide, opacity: 0.4)
}
.padding()
Spacer() Spacer()
@ -122,5 +156,11 @@ struct EmulationView: View {
isInBackground = true isInBackground = true
} }
} }
.sheet(isPresented: $showSettings) {
// PerGameSettingsView(titleId: startgame?.titleId ?? "", manager: InGameSettingsManager.shared)
// .onDisappear() {
// InGameSettingsManager.shared.saveSettings()
// }
}
} }
} }

View file

@ -0,0 +1,62 @@
//
// InGameSettingsManager.swift
// MeloNX
//
// Created by Stossy11 on 12/06/2025.
//
import Foundation
class InGameSettingsManager: PerGameSettingsManaging {
@Published var config: [String: Ryujinx.Arguments]
private var saveWorkItem: DispatchWorkItem?
public static var shared = InGameSettingsManager()
private init() {
self.config = PerGameSettingsManager.loadSettings() ?? [:]
}
func debouncedSave() {
saveWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
self.saveSettings()
}
saveWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
}
func saveSettings() {
if let currentgame = Ryujinx.shared.games.first(where: { $0.fileURL == URL(string: Ryujinx.shared.config?.gamepath ?? "") }) {
Ryujinx.shared.config = config[currentgame.titleId]
let args = Ryujinx.shared.buildCommandLineArgs(from: config[currentgame.titleId] ?? Ryujinx.Arguments())
// Convert Arguments to ones that Ryujinx can Read
let cArgs = args.map { strdup($0) }
defer { cArgs.forEach { free($0) } }
var argvPtrs = cArgs
let result = update_settings_external(Int32(args.count), &argvPtrs)
print(result)
}
}
static func loadSettings() -> [String: Ryujinx.Arguments]? {
var cool: [String: Ryujinx.Arguments] = [:]
if let currentgame = Ryujinx.shared.games.first(where: { $0.fileURL == URL(string: Ryujinx.shared.config?.gamepath ?? "") }) {
cool[currentgame.titleId] = Ryujinx.shared.config
return cool
} else {
return nil
}
}
func loadSettings() {
self.config = PerGameSettingsManager.loadSettings() ?? [:]
}
}

View file

@ -357,8 +357,16 @@ struct ContentView: View {
} }
} }
@StateObject private var persettings = PerGameSettingsManager.shared
private func start(displayid: UInt32) { private func start(displayid: UInt32) {
guard let game else { return } guard let game else { return }
var config = self.config
persettings.loadSettings()
if let customgame = persettings.config[game.titleId] {
config = customgame
}
config.gamepath = game.fileURL.path config.gamepath = game.fileURL.path
config.inputids = Array(Set(currentControllers.map(\.id))) config.inputids = Array(Set(currentControllers.map(\.id)))
@ -367,9 +375,7 @@ struct ContentView: View {
registerMotionForMatchingControllers() registerMotionForMatchingControllers()
if config.inputids.isEmpty { config.inputids.isEmpty ? config.inputids.append("0") : ()
config.inputids.append("0")
}
// Local DSU loopback to ryujinx per input id // Local DSU loopback to ryujinx per input id
for _ in config.inputids { for _ in config.inputids {

View file

@ -26,6 +26,15 @@ struct GameLibraryView: View {
@State var startgame = false @State var startgame = false
@State var isSelectingGameFile = false @State var isSelectingGameFile = false
@State var isViewingGameInfo: Bool = false @State var isViewingGameInfo: Bool = false
@State var gamePerGameSettings: Game?
var isShowingPerGameSettings: Binding<Bool> {
Binding<Bool> {
gamePerGameSettings != nil
} set: { value in
!value ? gamePerGameSettings = nil : ()
}
}
@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 @StateObject var ryujinx = Ryujinx.shared
@ -201,6 +210,9 @@ struct GameLibraryView: View {
.sheet(isPresented: $isSelectingGameDLC) { .sheet(isPresented: $isSelectingGameDLC) {
DLCManagerSheet(game: $gameInfo) DLCManagerSheet(game: $gameInfo)
} }
.sheet(isPresented: isShowingPerGameSettings) {
PerGameSettingsView(titleId: gamePerGameSettings!.titleId)
}
.sheet(isPresented: Binding( .sheet(isPresented: Binding(
get: { isViewingGameInfo && gameInfo != nil }, get: { isViewingGameInfo && gameInfo != nil },
set: { newValue in set: { newValue in
@ -271,7 +283,8 @@ struct GameLibraryView: View {
isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameUpdate: $isSelectingGameUpdate,
isSelectingGameDLC: $isSelectingGameDLC, isSelectingGameDLC: $isSelectingGameDLC,
gameRequirements: $gameRequirements, gameRequirements: $gameRequirements,
gameInfo: $gameInfo gameInfo: $gameInfo,
perGameSettings: $gamePerGameSettings
) )
.padding(.horizontal) .padding(.horizontal)
.padding(.vertical, 8) .padding(.vertical, 8)
@ -288,7 +301,8 @@ struct GameLibraryView: View {
isSelectingGameUpdate: $isSelectingGameUpdate, isSelectingGameUpdate: $isSelectingGameUpdate,
isSelectingGameDLC: $isSelectingGameDLC, isSelectingGameDLC: $isSelectingGameDLC,
gameRequirements: $gameRequirements, gameRequirements: $gameRequirements,
gameInfo: $gameInfo gameInfo: $gameInfo,
perGameSettings: $gamePerGameSettings
) )
.padding(.horizontal) .padding(.horizontal)
.padding(.vertical, 8) .padding(.vertical, 8)
@ -482,6 +496,12 @@ struct GameLibraryView: View {
} label: { } label: {
Label("Game Info", systemImage: "info.circle") Label("Game Info", systemImage: "info.circle")
} }
Button {
gamePerGameSettings = game
} label: {
Label("\(game.titleName) Settings", systemImage: "gear")
}
} }
Section { Section {
@ -501,6 +521,12 @@ struct GameLibraryView: View {
} }
Section { Section {
Button(role: .destructive) {
removeFromRecentGames(game)
} label: {
Label("Remove from Recents", systemImage: "trash")
}
if #available(iOS 15, *) { if #available(iOS 15, *) {
Button(role: .destructive) { Button(role: .destructive) {
deleteGame(game: game) deleteGame(game: game)
@ -771,6 +797,8 @@ struct GameListRow: View {
@Binding var isSelectingGameDLC: Bool @Binding var isSelectingGameDLC: Bool
@Binding var gameRequirements: [GameRequirements] @Binding var gameRequirements: [GameRequirements]
@Binding var gameInfo: Game? @Binding var gameInfo: Game?
@StateObject private var settingsManager = PerGameSettingsManager.shared
@Binding var perGameSettings: Game?
@State var gametoDelete: Game? @State var gametoDelete: Game?
@State var showGameDeleteConfirmation: Bool = false @State var showGameDeleteConfirmation: Bool = false
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@ -828,6 +856,14 @@ struct GameListRow: View {
} }
} }
if $settingsManager.config.wrappedValue.contains(where: { $0.key == game.titleId }) {
Image(systemName: "gearshape.circle")
.resizable()
.aspectRatio(contentMode: .fill)
.foregroundStyle(.blue)
.frame(width: 20, height: 20)
}
Spacer() Spacer()
VStack(alignment: .leading) { VStack(alignment: .leading) {
@ -897,6 +933,12 @@ struct GameListRow: View {
} label: { } label: {
Label("Game Info", systemImage: "info.circle") Label("Game Info", systemImage: "info.circle")
} }
Button {
perGameSettings = game
} label: {
Label("\(game.titleName) Settings", systemImage: "gear")
}
} }
Section { Section {
@ -959,10 +1001,7 @@ struct GameListRow: View {
Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?") Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
} }
.listRowInsets(EdgeInsets()) .listRowInsets(EdgeInsets())
.background( .wow(colorScheme)
RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
)
} else { } else {
Button(action: { Button(action: {
startemu = game startemu = game
@ -1196,3 +1235,20 @@ func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Err
task.resume() task.resume()
} }
extension View {
func wow(_ colorScheme: ColorScheme) -> some View {
if #available(iOS 26.0, *) {
return self
.glassEffect(Glass.regular, in:
RoundedRectangle(cornerRadius: 12)
)
} else {
return self
.background(
RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
)
}
}
}

View file

@ -0,0 +1,236 @@
//
// AppIconSwitcher.swift
// MeloNX
//
// Created by Stossy11 on 02/06/2025.
//
import SwiftUI
struct AppIcon: Identifiable, Equatable {
var id: String { creator }
var iconNames: [String: String]
var creator: String
}
struct AppIconSwitcherView: View {
@Environment(\.dismiss) private var dismiss
@State var appIcons: [AppIcon] = [
AppIcon(iconNames: ["Default": UIImage.appIcon(), "Dark Mode": "DarkMode", "Round": "RoundAppIcon"], creator: "CycloKid"),
AppIcon(iconNames: ["Pixel Default": "PixelAppIcon", "Pixel Round": "PixelRoundAppIcon"], creator: "Nobody"),
AppIcon(iconNames: ["\"UwU\"": "uwuAppIcon"], creator: "𝒰𝓃𝓀𝓃𝑜𝓌𝓃")
]
@State var columns: [GridItem] = [
GridItem(.flexible(), spacing: 20),
GridItem(.flexible(), spacing: 20),
GridItem(.flexible(), spacing: 20)
]
@State private var currentIconName: String? = nil
@State var refresh = 0
var body: some View {
NavigationView {
ZStack {
LinearGradient(
gradient: Gradient(colors: [
Color(.systemBackground).opacity(0.95),
Color(.systemGroupedBackground)
]),
startPoint: .top,
endPoint: .bottom
)
.ignoresSafeArea()
ScrollView {
LazyVStack(spacing: 32) {
ForEach(appIcons.indices, id: \.self) { index in
let iconGroup = appIcons[index]
VStack(alignment: .leading, spacing: 20) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(iconGroup.creator)
.font(.title2)
.fontWeight(.bold)
.foregroundStyle(.primary)
Text("\(iconGroup.iconNames.count) icons")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.horizontal, 24)
LazyVGrid(columns: columns, spacing: 20) {
ForEach(Array(iconGroup.iconNames.keys.sorted()), id: \.self) { key in
if let iconName = iconGroup.iconNames[key] {
Button {
selectIcon(iconName)
} label: {
ZStack {
AppIconView(app: (iconName, key))
if iconName == currentIconName ?? UIImage.appIcon() {
VStack {
HStack {
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 24, weight: .bold))
.foregroundStyle(.white)
.background(
Circle()
.fill(
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 28, height: 28)
)
}
Spacer()
}
.frame(width: 80, height: 80)
.offset(x: 6, y: -6)
}
}
}
.buttonStyle(PlainButtonStyle())
.scaleEffect(isCurrentIcon(iconName) ? 0.95 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: isCurrentIcon(iconName))
}
}
}
.padding(.horizontal, 24)
}
// Stylized divider
if index < appIcons.count - 1 {
Rectangle()
.fill(
LinearGradient(
colors: [.clear, Color(.separator), .clear],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(height: 1)
.padding(.horizontal, 40)
}
}
}
.padding(.vertical, 32)
}
}
.navigationTitle("Choose App Icon")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(.blue)
}
}
}
.onAppear(perform: setupColumns)
.onAppear(perform: getCurrentIconName)
}
private func setupColumns() {
if #available(iOS 18.5, *) {
//
} else {
if checkforOld() {
if let value = appIcons[0].iconNames.removeValue(forKey: "Round") {
appIcons[0].iconNames["PomeloNX"] = value
}
if let value = appIcons[1].iconNames.removeValue(forKey: "Pixel Round") {
appIcons[1].iconNames["Pixel PomeloNX"] = value
}
}
}
}
private func getCurrentIconName() {
currentIconName = UIApplication.shared.alternateIconName ?? UIImage.appIcon()
}
private func isCurrentIcon(_ iconName: String) -> Bool {
let currentIcon = UIApplication.shared.alternateIconName ?? UIImage.appIcon()
return currentIcon == iconName
}
private func selectIcon(_ iconName: String) {
// Haptic feedback
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
if iconName == UIImage.appIcon() {
UIApplication.shared.setAlternateIconName(nil) { error in
if let error = error {
print("Error setting icon: \(error)")
} else {
DispatchQueue.main.async {
currentIconName = nil
refresh = Int.random(in: 0...100)
}
}
}
} else {
UIApplication.shared.setAlternateIconName(iconName) { error in
if let error = error {
print("Error setting icon: \(error)")
} else {
DispatchQueue.main.async {
currentIconName = iconName
refresh = Int.random(in: 0...100)
}
}
}
}
}
}
struct AppIconView: View {
let app: (String, String)
var body: some View {
VStack(spacing: 7) {
ZStack {
Image(uiImage: UIImage(named: app.0)!)
.resizable()
.cornerRadius(15)
.frame(width: 62, height: 62)
.shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
}
Text(app.1)
.font(.system(size: 12, weight: .medium))
.foregroundColor(.white)
.multilineTextAlignment(.center)
.shadow(color: .black.opacity(0.2), radius: 2, x: 0, y: 1)
.frame(width: 100)
.lineLimit(1)
}
}
}
extension UIImage {
static func appIcon() -> String {
if let icons = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String: Any],
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
let lastIcon = iconFiles.last {
return lastIcon
}
return "AppIcon"
}
}

View file

@ -0,0 +1,707 @@
//
// PerGameSettingsView.swift
// MeloNX
//
// Created by Stossy11 on 12/06/2025.
//
import SwiftUI
protocol PerGameSettingsManaging: ObservableObject {
var config: [String: Ryujinx.Arguments] { get set }
func debouncedSave()
func saveSettings()
func loadSettings()
static func loadSettings() -> [String: Ryujinx.Arguments]?
}
class PerGameSettingsManager: PerGameSettingsManaging {
@Published var config: [String: Ryujinx.Arguments] {
didSet {
debouncedSave()
}
}
private var saveWorkItem: DispatchWorkItem?
public static var shared = PerGameSettingsManager()
private init() {
self.config = PerGameSettingsManager.loadSettings() ?? [:]
}
func debouncedSave() {
saveWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
self.saveSettings()
}
saveWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
}
func saveSettings() {
do {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(config)
let fileURL = URL.documentsDirectory.appendingPathComponent("config-pergame.json")
try data.write(to: fileURL)
print("Settings saved successfully")
} catch {
print("Failed to save settings: \(error)")
}
}
static func loadSettings() -> [String: Ryujinx.Arguments]? {
do {
let fileURL = URL.documentsDirectory.appendingPathComponent("config-pergame.json")
guard FileManager.default.fileExists(atPath: fileURL.path) else {
print("Config file does not exist, creating new config")
return nil
}
let data = try Data(contentsOf: fileURL)
let decoder = JSONDecoder()
let configs = try decoder.decode([String: Ryujinx.Arguments].self, from: data)
return configs
} catch {
print("Failed to load settings: \(error)")
return nil
}
}
func loadSettings() {
self.config = PerGameSettingsManager.loadSettings() ?? [:]
}
}
struct PerGameSettingsView: View {
@StateObject private var settingsManager: PerGameSettingsManager
var titleId: String
init(titleId: String, manager: any PerGameSettingsManaging = PerGameSettingsManager.shared) {
self._settingsManager = StateObject(wrappedValue: manager as! PerGameSettingsManager)
self.titleId = titleId
}
private var config: Binding<Ryujinx.Arguments> {
return Binding<Ryujinx.Arguments> {
return settingsManager.config[titleId] ?? Ryujinx.Arguments()
} set: { newValue in
settingsManager.config[titleId] = newValue
settingsManager.debouncedSave()
}
}
var memoryManagerModes = [
("HostMapped", "Host (fast)"),
("HostMappedUnsafe", "Host Unchecked (fast, unstable / unsafe)"),
("SoftwarePageTable", "Software (slow)"),
]
let totalMemory = ProcessInfo.processInfo.physicalMemory
@State private var showResolutionInfo = false
@State private var showAnisotropicInfo = false
@State private var showControllerInfo = false
@State private var showAppIconSwitcher = false
@State private var searchText = ""
@StateObject var ryujinx = Ryujinx.shared
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
@State private var selectedCategory: PerSettingsCategory = .graphics
@StateObject var metalHudEnabler = MTLHud.shared
var filteredMemoryModes: [(String, String)] {
guard !searchText.isEmpty else { return memoryManagerModes }
return memoryManagerModes.filter { $0.1.localizedCaseInsensitiveContains(searchText) }
}
var appVersion: String {
guard let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
return "Unknown"
}
return version
}
@FocusState private var isArgumentsKeyboardVisible: Bool
@State private var selectedView = "Data Management"
@State private var sidebar = true
enum PerSettingsCategory: String, CaseIterable, Identifiable {
case graphics = "Graphics"
case system = "System"
case advanced = "Advanced"
var id: String { self.rawValue }
var icon: String {
switch self {
case .graphics: return "paintbrush.fill"
case .system: return "gearshape.fill"
case .advanced: return "terminal.fill"
}
}
}
var body: some View {
iOSNav {
ZStack {
Color(UIColor.systemBackground)
.ignoresSafeArea()
VStack(spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(PerSettingsCategory.allCases, id: \.id) { category in
CategoryButton(
title: category.rawValue,
icon: category.icon,
isSelected: selectedCategory == category
) {
selectedCategory = category
}
}
}
.padding(.horizontal)
.padding(.vertical, 8)
}
Divider()
// Settings content
ScrollView {
VStack(spacing: 24) {
switch selectedCategory {
case .graphics:
graphicsSettings
.padding(.top)
case .system:
systemSettings
.padding(.top)
case .advanced:
advancedSettings
.padding(.top)
}
Spacer(minLength: 50)
}
.padding(.bottom)
}
.scrollDismissesKeyboardIfAvailable()
}
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
settingsManager.debouncedSave()
dismiss()
}
}
ToolbarItem(placement: .cancellationAction) {
Button("Reset") {
dismiss()
settingsManager.config[titleId] = nil
settingsManager.saveSettings()
}
}
}
// .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .automatic))
.onAppear {
// if let configs = SettingsManager.loadSettings() {
// settingsManager.loadSettings()
// } else {
// settingsManager.saveSettings()
//}
print(titleId)
if settingsManager.config[titleId] == nil {
settingsManager.config[titleId] = Ryujinx.Arguments()
settingsManager.debouncedSave()
}
}
}
}
// MARK: - Graphics Settings
private var graphicsSettings: some View {
SettingsSection(title: "Graphics & Performance") {
// Resolution scale card
SettingsCard {
VStack(alignment: .leading, spacing: 12) {
HStack {
labelWithIcon("Resolution Scale", iconName: "magnifyingglass")
.font(.headline)
Spacer()
Button {
showResolutionInfo.toggle()
} label: {
Image(systemName: "info.circle")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
.alert(isPresented: $showResolutionInfo) {
Alert(
title: Text("Resolution Scale"),
message: Text("Adjust the internal rendering resolution. Higher values improve visuals but may reduce performance."),
dismissButton: .default(Text("OK"))
)
}
}
VStack(spacing: 8) {
Slider(value: config.resscale, in: 0.1...3.0, step: 0.05)
HStack {
Text("0.1x")
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text("\(config.resscale.wrappedValue, specifier: "%.2f")x")
.font(.headline)
.foregroundColor(.blue)
Spacer()
Text("3.0x")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
}
// Anisotropic filtering card
SettingsCard {
VStack(alignment: .leading, spacing: 12) {
HStack {
labelWithIcon("Max Anisotropic Filtering", iconName: "magnifyingglass")
.font(.headline)
Spacer()
Button {
showAnisotropicInfo.toggle()
} label: {
Image(systemName: "info.circle")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
.alert(isPresented: $showAnisotropicInfo) {
Alert(
title: Text("Max Anisotropic Filtering"),
message: Text("Adjust the internal Anisotropic filtering. Higher values improve texture quality at angles but may reduce performance. Default at 0 lets game decide."),
dismissButton: .default(Text("OK"))
)
}
}
VStack(spacing: 8) {
Slider(value: config.maxAnisotropy, in: 0...16.0, step: 0.1)
HStack {
Text("Off")
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text("\(config.maxAnisotropy.wrappedValue, specifier: "%.1f")x")
.font(.headline)
.foregroundColor(.blue)
Spacer()
Text("16x")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
}
// Toggle options card
SettingsCard {
VStack(spacing: 4) {
PerSettingsToggle(isOn: config.disableShaderCache, icon: "memorychip", label: "Shader Cache")
Divider()
PerSettingsToggle(isOn: config.disablevsync, icon: "arrow.triangle.2.circlepath", label: "Disable VSync")
Divider()
PerSettingsToggle(isOn: config.enableTextureRecompression, icon: "rectangle.compress.vertical", label: "Texture Recompression")
Divider()
PerSettingsToggle(isOn: config.disableDockedMode, icon: "dock.rectangle", label: "Docked Mode")
Divider()
PerSettingsToggle(isOn: config.macroHLE, icon: "gearshape", label: "Macro HLE")
}
}
// Aspect ratio card
SettingsCard {
VStack(alignment: .leading, spacing: 12) {
labelWithIcon("Aspect Ratio", iconName: "rectangle.expand.vertical")
.font(.headline)
if (horizontalSizeClass == .regular && verticalSizeClass == .regular) || (horizontalSizeClass == .regular && verticalSizeClass == .compact) {
Picker(selection: config.aspectRatio) {
ForEach(AspectRatio.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
EmptyView()
}
.pickerStyle(.segmented)
} else {
Picker(selection: config.aspectRatio) {
ForEach(AspectRatio.allCases, id: \.self) { ratio in
Text(ratio.displayName).tag(ratio)
}
} label: {
EmptyView()
}
.pickerStyle(.menu)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
}
}
}
}
}
// MARK: - System Settings
private var systemSettings: some View {
SettingsSection(title: "System Configuration") {
// Language and region card
SettingsCard {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
labelWithIcon("System Language", iconName: "character.bubble")
.font(.headline)
Picker(selection: config.language) {
ForEach(SystemLanguage.allCases, id: \.self) { language in
Text(language.displayName).tag(language)
}
} label: {
EmptyView()
}
.pickerStyle(.menu)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
}
Divider()
VStack(alignment: .leading, spacing: 8) {
labelWithIcon("Region", iconName: "globe")
.font(.headline)
Picker(selection: config.regioncode) {
ForEach(SystemRegionCode.allCases, id: \.self) { region in
Text(region.displayName).tag(region)
}
} label: {
EmptyView()
}
.pickerStyle(.menu)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 4)
}
}
}
// CPU options card
SettingsCard {
VStack(alignment: .leading, spacing: 16) {
Text("CPU Configuration")
.font(.headline)
.foregroundColor(.primary)
VStack(alignment: .leading, spacing: 8) {
Text("Memory Manager Mode")
.font(.subheadline)
.foregroundColor(.secondary)
Picker(selection: config.memoryManagerMode) {
ForEach(filteredMemoryModes, id: \.0) { key, displayName in
Text(displayName).tag(key)
}
} label: {
EmptyView()
}
.pickerStyle(.segmented)
}
Divider()
PerSettingsToggle(isOn: config.disablePTC, icon: "cpu", label: "Disable PTC")
if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
Divider()
if #available(iOS 16.4, *) {
PerSettingsToggle(isOn: .constant(false), icon: "bolt", label: "Hypervisor")
.disabled(true)
} else if checkAppEntitlement("com.apple.private.hypervisor") {
PerSettingsToggle(isOn: config.hypervisor, icon: "bolt", label: "Hypervisor")
}
}
}
}
// Controller options card
SettingsCard {
VStack(alignment: .leading, spacing: 16) {
Text("Controller Configuration")
.font(.headline)
.foregroundColor(.primary)
PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
}
}
}
}
// MARK: - Advanced Settings
private var advancedSettings: some View {
SettingsSection(title: "Advanced Options") {
// Debug options card
SettingsCard {
VStack(spacing: 4) {
PerSettingsToggle(isOn: config.debuglogs, icon: "exclamationmark.bubble", label: "Debug Logs")
Divider()
PerSettingsToggle(isOn: config.tracelogs, icon: "waveform.path", label: "Trace Logs")
}
}
// Advanced toggles card
SettingsCard {
VStack(spacing: 4) {
PerSettingsToggle(isOn: config.dfsIntegrityChecks, icon: "checkmark.shield", label: "Disable FS Integrity Checks")
.accentColor(.red)
Divider()
PerSettingsToggle(isOn: config.expandRam, icon: "exclamationmark.bubble", label: "Expand Guest RAM")
.accentColor(.red)
.disabled(totalMemory < 5723)
Divider()
PerSettingsToggle(isOn: config.ignoreMissingServices, icon: "waveform.path", label: "Ignore Missing Services")
.accentColor(.red)
}
}
// Additional args card
SettingsCard {
VStack(alignment: .leading, spacing: 12) {
Text("Additional Arguments")
.font(.headline)
.foregroundColor(.primary)
let binding = Binding(
get: {
config.additionalArgs.wrappedValue.joined(separator: ", ")
},
set: { newValue in
let args = newValue
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
config.additionalArgs.wrappedValue = args
}
)
if #available(iOS 15.0, *) {
TextField("Separate arguments with commas", text: binding)
.font(.system(.body, design: .monospaced))
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.none)
.disableAutocorrection(true)
.padding(.vertical, 4)
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Dismiss") {
isArgumentsKeyboardVisible = false
}
}
}
.focused($isArgumentsKeyboardVisible)
} else {
TextField("Separate arguments with commas", text: binding)
.font(.system(.body, design: .monospaced))
.textFieldStyle(.roundedBorder)
.disableAutocorrection(true)
.padding(.vertical, 4)
}
}
}
// Page size info card
SettingsCard {
HStack {
labelWithIcon("Page Size", iconName: "textformat.size")
Spacer()
Text("\(String(Int(getpagesize())))")
.font(.system(.body, design: .monospaced))
.foregroundColor(.secondary)
}
}
}
}
// MARK: - Miscellaneous Settings
private var miscSettings: some View {
SettingsSection(title: "Miscellaneous Options") {
SettingsCard {
VStack(spacing: 4) {
PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
}
}
}
}
// MARK: - Helper Functions
func getGPUInfo() -> String? {
let device = MTLCreateSystemDefaultDevice()
return device?.name
}
@ViewBuilder
private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
HStack(spacing: 8) {
if iconName.hasSuffix(".svg") {
if let flipimage, flipimage {
SVGView(svgName: iconName, color: .blue)
// .symbolRenderingMode(.hierarchical)
.frame(width: 20, height: 20)
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
} else {
SVGView(svgName: iconName, color: .blue)
// .symbolRenderingMode(.hierarchical)
.frame(width: 20, height: 20)
}
} else if !iconName.isEmpty {
Image(systemName: iconName)
// .symbolRenderingMode(.hierarchical)
.foregroundColor(.blue)
}
Text(text)
}
.font(.body)
}
}
// MARK: - Supporting Views
// PerSettingsToggle(isOn: config.handHeldController, icon: "formfitting.gamecontroller", label: "Player 1 to Handheld")
struct PerSettingsCard<Content: View>: View {
@Environment(\.colorScheme) var colorScheme
@AppStorage("oldSettingsUI") var oldSettingsUI = false
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(colorScheme == .dark ? Color(.systemGray6) : Color.white)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
)
.padding(.horizontal)
}
}
struct PerSettingsToggle: View {
@Binding var isOn: Bool
let icon: String
let label: String
var disabled: Bool = false
@AppStorage("toggleGreen") var toggleGreen: Bool = false
@AppStorage("oldSettingsUI") var oldSettingsUI = false
var body: some View {
Toggle(isOn: $isOn) {
HStack(spacing: 8) {
if icon.hasSuffix(".svg") {
SVGView(svgName: icon, color: .blue)
.frame(width: 20, height: 20)
} else {
Image(systemName: icon)
// .symbolRenderingMode(.hierarchical)
.foregroundColor(.blue)
}
Text(label)
.font(.body)
}
}
.toggleStyle(SwitchToggleStyle(tint: .blue))
.disabled(disabled)
.padding(.vertical, 6)
}
func disabled(_ disabled: Bool) -> PerSettingsToggle {
var view = self
view.disabled = disabled
return view
}
func accentColor(_ color: Color) -> PerSettingsToggle {
var view = self
return view
}
}

View file

@ -266,6 +266,8 @@ struct SettingsViewNew: View {
@AppStorage("runOnMainThread") var runOnMainThread = false @AppStorage("runOnMainThread") var runOnMainThread = false
@AppStorage("oldSettingsUI") var oldSettingsUI = false
@AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState() @AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
let totalMemory = ProcessInfo.processInfo.physicalMemory let totalMemory = ProcessInfo.processInfo.physicalMemory
@ -275,6 +277,7 @@ struct SettingsViewNew: View {
@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 showAppIconSwitcher = false
@State private var searchText = "" @State private var searchText = ""
@AppStorage("portal") var gamepo = false @AppStorage("portal") var gamepo = false
@StateObject var ryujinx = Ryujinx.shared @StateObject var ryujinx = Ryujinx.shared
@ -327,10 +330,12 @@ struct SettingsViewNew: View {
var body: some View { var body: some View {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {
iOSSettings iOSSettings
} else { } else if !oldSettingsUI {
iPadOSSettings iPadOSSettings
.ignoresSafeArea() .ignoresSafeArea()
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
} else {
iOSSettings
} }
} }
@ -1188,7 +1193,6 @@ struct SettingsViewNew: View {
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {
TextField("Separate arguments with commas", text: binding) TextField("Separate arguments with commas", text: binding)
.font(.system(.body, design: .monospaced)) .font(.system(.body, design: .monospaced))
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@ -1254,13 +1258,31 @@ struct SettingsViewNew: View {
Divider() Divider()
if colorScheme == .light { if colorScheme == .light {
SettingsToggle(isOn: $disableTouch, icon: "iphone.slash", label: "Black Screen when using AirPlay") SettingsToggle(isOn: $blackScreen, icon: "iphone.slash", label: "Black Screen when using AirPlay")
Divider() Divider()
} }
Button {
showAppIconSwitcher = true
} label: {
HStack {
Image(systemName: "app.dashed")
.foregroundColor(.blue)
Text("App Icon Switcher")
.foregroundColor(.primary)
Spacer()
}
.padding(.vertical, 8)
}
.sheet(isPresented: $showAppIconSwitcher) {
AppIconSwitcherView()
}
Divider()
// Exit button card // Exit button card
SettingsToggle(isOn: $ssb, icon: "arrow.left.circle", label: "Exit Button") SettingsToggle(isOn: $ssb, icon: "arrow.left.circle", label: "Menu Button (in-game)")
Divider() Divider()
@ -1275,6 +1297,13 @@ struct SettingsViewNew: View {
Divider() Divider()
if UIDevice.current.userInterfaceIdiom == .pad {
// Old Settings UI
SettingsToggle(isOn: $oldSettingsUI, icon: "ipad.landscape", label: "Non Switch-like Settings")
Divider()
}
// JIT options // JIT options
if #available(iOS 17.0.1, *) { if #available(iOS 17.0.1, *) {
@ -1401,8 +1430,6 @@ struct SVGView: UIViewRepresentable {
svgName.removeLast(4) svgName.removeLast(4)
} }
_ = UIView(svgNamed: svgName) { svgLayer in _ = UIView(svgNamed: svgName) { svgLayer in
svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
svgLayer.resizeToFit(hammock.frame) svgLayer.resizeToFit(hammock.frame)
@ -1505,6 +1532,7 @@ struct SettingsSection<Content: View>: View {
struct SettingsCard<Content: View>: View { struct SettingsCard<Content: View>: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@AppStorage("oldSettingsUI") var oldSettingsUI = false
let content: Content let content: Content
init(@ViewBuilder content: () -> Content) { init(@ViewBuilder content: () -> Content) {
@ -1512,7 +1540,7 @@ struct SettingsCard<Content: View>: View {
} }
var body: some View { var body: some View {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone || oldSettingsUI {
content content
.padding() .padding()
.background( .background(
@ -1538,9 +1566,10 @@ struct SettingsToggle: View {
let label: String let label: String
var disabled: Bool = false var disabled: Bool = false
@AppStorage("toggleGreen") var toggleGreen: Bool = false @AppStorage("toggleGreen") var toggleGreen: Bool = false
@AppStorage("oldSettingsUI") var oldSettingsUI = false
var body: some View { var body: some View {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone || oldSettingsUI {
Toggle(isOn: $isOn) { Toggle(isOn: $isOn) {
HStack(spacing: 8) { HStack(spacing: 8) {
if icon.hasSuffix(".svg") { if icon.hasSuffix(".svg") {

View file

@ -40,40 +40,61 @@ struct MeloNXApp: App {
@AppStorage("autoJIT") var autoJIT = false @AppStorage("autoJIT") var autoJIT = false
@State var fourgbiPad = false
@AppStorage("4GB iPad") var ignores = false
// String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000)
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
if finishedStorage { Group {
ContentView() if finishedStorage {
.withFileImporter() ContentView()
.onAppear { .withFileImporter()
if checkForUpdate { .onAppear {
checkLatestVersion() if checkForUpdate {
checkLatestVersion()
}
print(metalHudEnabler.canMetalHud)
UserDefaults.standard.set(false, forKey: "lockInApp")
} }
.sheet(isPresented: Binding(
print(metalHudEnabler.canMetalHud) get: { showOutOfDateSheet && updateInfo != nil },
set: { newValue in
UserDefaults.standard.set(false, forKey: "lockInApp") if !newValue {
} showOutOfDateSheet = false
.sheet(isPresented: Binding( updateInfo = nil
get: { showOutOfDateSheet && updateInfo != nil }, }
set: { newValue in }
if !newValue { )) {
showOutOfDateSheet = false if let updateInfo = updateInfo {
updateInfo = nil MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
} }
} }
)) { } else {
if let updateInfo = updateInfo { SetupView(finished: $finished)
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet) .onChange(of: finished) { newValue in
} withAnimation(.easeOut) {
} finishedStorage = newValue
} else { }
SetupView(finished: $finished)
.onChange(of: finished) { newValue in
withAnimation(.easeOut) {
finishedStorage = newValue
} }
}
}
.onAppear() {
if UIDevice.current.userInterfaceIdiom == .pad && !ignores {
print((Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000))
if round(Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000) <= 4 {
fourgbiPad = true
} }
}
}
.alert("Unsupported Device", isPresented: $fourgbiPad) {
Button("Continue") {
ignores = true
fourgbiPad = false
}
} message: {
Text("Your Device is an iPad with \(String(format: "%.0f GB", Double(ProcessInfo.processInfo.physicalMemory) / 1_000_000_000)) of memory, MeloNX has issues with those devices")
} }
} }
} }
@ -122,3 +143,8 @@ struct MeloNXApp: App {
task.resume() task.resume()
} }
} }
func changeAppUI(_ string: String) -> String? {
guard let data = Data(base64Encoded: string) else { return nil }
return String(data: data, encoding: .utf8)
}

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "darker.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "darker.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "MeloNX 1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "MeloNX 1024.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "PixelPomeloNX 1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "PixelPomeloNX 1024.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "copycat.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "copycat.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

View file

@ -0,0 +1,36 @@
{
"images" : [
{
"filename" : "melowonx.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View file

@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "melowonx.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View file

@ -10,7 +10,7 @@
</data> </data>
<key>Info.plist</key> <key>Info.plist</key>
<data> <data>
UOH9NuuEcz5NQiQlrM2LNFaG2pI= RTwvCLsTMs+YfZ9ZeF25QYe7/LE=
</data> </data>
<key>Modules/module.modulemap</key> <key>Modules/module.modulemap</key>
<data> <data>

View file

@ -4,6 +4,10 @@
<dict> <dict>
<key>files</key> <key>files</key>
<dict> <dict>
<key>.DS_Store</key>
<data>
7Mfr8shT4pXWBr/plN+uNkIabdM=
</data>
<key>Headers/StosJIT-Swift.h</key> <key>Headers/StosJIT-Swift.h</key>
<data> <data>
h9vaTwhC6FlnyKmIkaxLQGlFd1g= h9vaTwhC6FlnyKmIkaxLQGlFd1g=
@ -26,7 +30,7 @@
</data> </data>
<key>Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo</key> <key>Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo</key>
<data> <data>
2mJoWBgg56N+3OxKfIDMLZFNHVk= nihJghwM5m7kxkQD7UvrWyHkLy8=
</data> </data>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key> <key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key>
<data> <data>
@ -79,7 +83,7 @@
<dict> <dict>
<key>hash2</key> <key>hash2</key>
<data> <data>
sZBe57nozztJzv83RPLjKIRYGSQmeE7XYCqr63xZONM= +Ehvco7cQbAaF7zufvBYTiGXFp37Hjym/Pav514sGPk=
</data> </data>
</dict> </dict>
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key> <key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key>

View file

@ -159,7 +159,14 @@ namespace Ryujinx.Graphics.GAL.Multithreading
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RunCommand(Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer) public static void RunCommand(Span<byte> memory, ThreadedRenderer threaded, IRenderer renderer)
{ {
_lookup[memory[^1]](memory, threaded, renderer); try
{
_lookup[memory[^1]](memory, threaded, renderer);
}
catch (Exception ex)
{
// I have no idea what i'm doing, doing this to see if i can avoid MoltenVK crashes in games.
}
} }
} }
} }

View file

@ -31,9 +31,16 @@ namespace Ryujinx.Graphics.Vulkan
_gd = gd; _gd = gd;
_device = device; _device = device;
// Create a template from the set usages. Assumes the descriptor set is updated in segment order then binding order. // Calculate total number of individual descriptors
int totalDescriptors = 0;
for (int seg = 0; seg < segments.Length; seg++)
{
totalDescriptors += segments[seg].Count;
}
DescriptorUpdateTemplateEntry* entries = stackalloc DescriptorUpdateTemplateEntry[segments.Length];
DescriptorUpdateTemplateEntry* entries = stackalloc DescriptorUpdateTemplateEntry[totalDescriptors];
int entryIndex = 0;
nuint structureOffset = 0; nuint structureOffset = 0;
for (int seg = 0; seg < segments.Length; seg++) for (int seg = 0; seg < segments.Length; seg++)
@ -42,45 +49,36 @@ namespace Ryujinx.Graphics.Vulkan
int binding = segment.Binding; int binding = segment.Binding;
int count = segment.Count; int count = segment.Count;
DescriptorType descriptorType = segment.Type.Convert();
if (IsBufferType(segment.Type)) // Create separate entries for each descriptor in this segment
for (int i = 0; i < count; i++)
{ {
entries[seg] = new DescriptorUpdateTemplateEntry() nuint stride;
if (IsBufferType(segment.Type))
{ {
DescriptorType = segment.Type.Convert(), stride = (nuint)Unsafe.SizeOf<DescriptorBufferInfo>();
DstBinding = (uint)binding, }
DescriptorCount = (uint)count, else if (IsBufferTextureType(segment.Type))
{
stride = (nuint)Unsafe.SizeOf<BufferView>();
}
else
{
stride = (nuint)Unsafe.SizeOf<DescriptorImageInfo>();
}
entries[entryIndex] = new DescriptorUpdateTemplateEntry()
{
DescriptorType = descriptorType,
DstBinding = (uint)(binding + i),
DescriptorCount = 1, // Always 1 descriptor per entry
Offset = structureOffset, Offset = structureOffset,
Stride = (nuint)Unsafe.SizeOf<DescriptorBufferInfo>() Stride = stride
}; };
structureOffset += (nuint)(Unsafe.SizeOf<DescriptorBufferInfo>() * count); structureOffset += stride;
} entryIndex++;
else if (IsBufferTextureType(segment.Type))
{
entries[seg] = new DescriptorUpdateTemplateEntry()
{
DescriptorType = segment.Type.Convert(),
DstBinding = (uint)binding,
DescriptorCount = (uint)count,
Offset = structureOffset,
Stride = (nuint)Unsafe.SizeOf<BufferView>()
};
structureOffset += (nuint)(Unsafe.SizeOf<BufferView>() * count);
}
else
{
entries[seg] = new DescriptorUpdateTemplateEntry()
{
DescriptorType = segment.Type.Convert(),
DstBinding = (uint)binding,
DescriptorCount = (uint)count,
Offset = structureOffset,
Stride = (nuint)Unsafe.SizeOf<DescriptorImageInfo>()
};
structureOffset += (nuint)(Unsafe.SizeOf<DescriptorImageInfo>() * count);
} }
} }
@ -89,7 +87,7 @@ namespace Ryujinx.Graphics.Vulkan
var info = new DescriptorUpdateTemplateCreateInfo() var info = new DescriptorUpdateTemplateCreateInfo()
{ {
SType = StructureType.DescriptorUpdateTemplateCreateInfo, SType = StructureType.DescriptorUpdateTemplateCreateInfo,
DescriptorUpdateEntryCount = (uint)segments.Length, DescriptorUpdateEntryCount = (uint)totalDescriptors,
PDescriptorUpdateEntries = entries, PDescriptorUpdateEntries = entries,
TemplateType = DescriptorUpdateTemplateType.DescriptorSet, TemplateType = DescriptorUpdateTemplateType.DescriptorSet,
@ -124,23 +122,6 @@ namespace Ryujinx.Graphics.Vulkan
int entry = 0; int entry = 0;
nuint structureOffset = 0; nuint structureOffset = 0;
void AddBinding(int binding, int count)
{
entries[entry++] = new DescriptorUpdateTemplateEntry()
{
DescriptorType = DescriptorType.UniformBuffer,
DstBinding = (uint)binding,
DescriptorCount = (uint)count,
Offset = structureOffset,
Stride = (nuint)Unsafe.SizeOf<DescriptorBufferInfo>()
};
structureOffset += (nuint)(Unsafe.SizeOf<DescriptorBufferInfo>() * count);
}
int startBinding = 0;
int bindingCount = 0;
foreach (ResourceDescriptor descriptor in descriptors.Descriptors) foreach (ResourceDescriptor descriptor in descriptors.Descriptors)
{ {
for (int i = 0; i < descriptor.Count; i++) for (int i = 0; i < descriptor.Count; i++)
@ -149,28 +130,21 @@ namespace Ryujinx.Graphics.Vulkan
if ((updateMask & (1L << binding)) != 0) if ((updateMask & (1L << binding)) != 0)
{ {
if (bindingCount > 0 && (RenderdocPushCountBug || startBinding + bindingCount != binding)) entries[entry] = new DescriptorUpdateTemplateEntry()
{ {
AddBinding(startBinding, bindingCount); DescriptorType = DescriptorType.UniformBuffer,
DstBinding = (uint)binding,
DescriptorCount = 1, // Always 1 descriptor per entry
Offset = structureOffset,
Stride = (nuint)Unsafe.SizeOf<DescriptorBufferInfo>()
};
bindingCount = 0; structureOffset += (nuint)Unsafe.SizeOf<DescriptorBufferInfo>();
} entry++;
if (bindingCount == 0)
{
startBinding = binding;
}
bindingCount++;
} }
} }
} }
if (bindingCount > 0)
{
AddBinding(startBinding, bindingCount);
}
Size = (int)structureOffset; Size = (int)structureOffset;
var info = new DescriptorUpdateTemplateCreateInfo() var info = new DescriptorUpdateTemplateCreateInfo()

View file

@ -3,10 +3,10 @@ using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader; using Ryujinx.Graphics.Shader;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using System; using System;
using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Buffer = Silk.NET.Vulkan.Buffer;
using CompareOp = Ryujinx.Graphics.GAL.CompareOp; using CompareOp = Ryujinx.Graphics.GAL.CompareOp;
using Format = Ryujinx.Graphics.GAL.Format; using Format = Ryujinx.Graphics.GAL.Format;
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo; using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
@ -141,11 +141,11 @@ namespace Ryujinx.Graphics.Vulkan
_bufferTextureRefs = new TextureBuffer[Constants.MaxTextureBindings * 2]; _bufferTextureRefs = new TextureBuffer[Constants.MaxTextureBindings * 2];
_bufferImageRefs = new TextureBuffer[Constants.MaxImageBindings * 2]; _bufferImageRefs = new TextureBuffer[Constants.MaxImageBindings * 2];
_textureArrayRefs = Array.Empty<ArrayRef<TextureArray>>(); _textureArrayRefs = [];
_imageArrayRefs = Array.Empty<ArrayRef<ImageArray>>(); _imageArrayRefs = [];
_textureArrayExtraRefs = Array.Empty<ArrayRef<TextureArray>>(); _textureArrayExtraRefs = [];
_imageArrayExtraRefs = Array.Empty<ArrayRef<ImageArray>>(); _imageArrayExtraRefs = [];
_uniformBuffers = new DescriptorBufferInfo[Constants.MaxUniformBufferBindings]; _uniformBuffers = new DescriptorBufferInfo[Constants.MaxUniformBufferBindings];
_storageBuffers = new DescriptorBufferInfo[Constants.MaxStorageBufferBindings]; _storageBuffers = new DescriptorBufferInfo[Constants.MaxStorageBufferBindings];
@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Vulkan
_uniformSetPd = new int[Constants.MaxUniformBufferBindings]; _uniformSetPd = new int[Constants.MaxUniformBufferBindings];
var initialImageInfo = new DescriptorImageInfo DescriptorImageInfo initialImageInfo = new()
{ {
ImageLayout = ImageLayout.General, ImageLayout = ImageLayout.General,
}; };
@ -217,7 +217,7 @@ namespace Ryujinx.Graphics.Vulkan
if (isMainPipeline) if (isMainPipeline)
{ {
FeedbackLoopHazards = new(); FeedbackLoopHazards = [];
} }
} }
@ -235,7 +235,7 @@ namespace Ryujinx.Graphics.Vulkan
// Check stage bindings // Check stage bindings
_uniformMirrored.Union(_uniformSet).SignalSet((int binding, int count) => _uniformMirrored.Union(_uniformSet).SignalSet((binding, count) =>
{ {
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
@ -257,7 +257,7 @@ namespace Ryujinx.Graphics.Vulkan
} }
}); });
_storageMirrored.Union(_storageSet).SignalSet((int binding, int count) => _storageMirrored.Union(_storageSet).SignalSet((binding, count) =>
{ {
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
@ -301,13 +301,13 @@ namespace Ryujinx.Graphics.Vulkan
{ {
for (int i = 0; i < segment.Count; i++) for (int i = 0; i < segment.Count; i++)
{ {
ref var texture = ref _textureRefs[segment.Binding + i]; ref TextureRef texture = ref _textureRefs[segment.Binding + i];
texture.View?.PrepareForUsage(cbs, texture.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards); texture.View?.PrepareForUsage(cbs, texture.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards);
} }
} }
else else
{ {
ref var arrayRef = ref _textureArrayRefs[segment.Binding]; ref ArrayRef<TextureArray> arrayRef = ref _textureArrayRefs[segment.Binding];
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags(); PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags); arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
} }
@ -322,13 +322,13 @@ namespace Ryujinx.Graphics.Vulkan
{ {
for (int i = 0; i < segment.Count; i++) for (int i = 0; i < segment.Count; i++)
{ {
ref var image = ref _imageRefs[segment.Binding + i]; ref ImageRef image = ref _imageRefs[segment.Binding + i];
image.View?.PrepareForUsage(cbs, image.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards); image.View?.PrepareForUsage(cbs, image.Stage.ConvertToPipelineStageFlags(), FeedbackLoopHazards);
} }
} }
else else
{ {
ref var arrayRef = ref _imageArrayRefs[segment.Binding]; ref ArrayRef<ImageArray> arrayRef = ref _imageArrayRefs[segment.Binding];
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags(); PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags); arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
} }
@ -337,7 +337,7 @@ namespace Ryujinx.Graphics.Vulkan
for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < _program.BindingSegments.Length; setIndex++) for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < _program.BindingSegments.Length; setIndex++)
{ {
var bindingSegments = _program.BindingSegments[setIndex]; ResourceBindingSegment[] bindingSegments = _program.BindingSegments[setIndex];
if (bindingSegments.Length == 0) if (bindingSegments.Length == 0)
{ {
@ -348,18 +348,18 @@ namespace Ryujinx.Graphics.Vulkan
if (segment.IsArray) if (segment.IsArray)
{ {
if (segment.Type == ResourceType.Texture || if (segment.Type is ResourceType.Texture or
segment.Type == ResourceType.Sampler || ResourceType.Sampler or
segment.Type == ResourceType.TextureAndSampler || ResourceType.TextureAndSampler or
segment.Type == ResourceType.BufferTexture) ResourceType.BufferTexture)
{ {
ref var arrayRef = ref _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts]; ref ArrayRef<TextureArray> arrayRef = ref _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags(); PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags); arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
} }
else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage) else if (segment.Type is ResourceType.Image or ResourceType.BufferImage)
{ {
ref var arrayRef = ref _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts]; ref ArrayRef<ImageArray> arrayRef = ref _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags(); PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags); arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
} }
@ -424,8 +424,8 @@ namespace Ryujinx.Graphics.Vulkan
{ {
for (int i = 0; i < buffers.Length; i++) for (int i = 0; i < buffers.Length; i++)
{ {
var assignment = buffers[i]; BufferAssignment assignment = buffers[i];
var buffer = assignment.Range; BufferRange buffer = assignment.Range;
int index = assignment.Binding; int index = assignment.Binding;
Auto<DisposableBuffer> vkBuffer = buffer.Handle == BufferHandle.Null Auto<DisposableBuffer> vkBuffer = buffer.Handle == BufferHandle.Null
@ -440,7 +440,7 @@ namespace Ryujinx.Graphics.Vulkan
Range = (ulong)buffer.Size, Range = (ulong)buffer.Size,
}; };
var newRef = new BufferRef(vkBuffer, ref buffer); BufferRef newRef = new(vkBuffer, ref buffer);
ref DescriptorBufferInfo currentInfo = ref _storageBuffers[index]; ref DescriptorBufferInfo currentInfo = ref _storageBuffers[index];
@ -460,7 +460,7 @@ namespace Ryujinx.Graphics.Vulkan
{ {
for (int i = 0; i < buffers.Length; i++) for (int i = 0; i < buffers.Length; i++)
{ {
var vkBuffer = buffers[i]; Auto<DisposableBuffer> vkBuffer = buffers[i];
int index = first + i; int index = first + i;
ref BufferRef currentBufferRef = ref _storageBufferRefs[index]; ref BufferRef currentBufferRef = ref _storageBufferRefs[index];
@ -633,8 +633,8 @@ namespace Ryujinx.Graphics.Vulkan
{ {
for (int i = 0; i < buffers.Length; i++) for (int i = 0; i < buffers.Length; i++)
{ {
var assignment = buffers[i]; BufferAssignment assignment = buffers[i];
var buffer = assignment.Range; BufferRange buffer = assignment.Range;
int index = assignment.Binding; int index = assignment.Binding;
Auto<DisposableBuffer> vkBuffer = buffer.Handle == BufferHandle.Null Auto<DisposableBuffer> vkBuffer = buffer.Handle == BufferHandle.Null
@ -678,7 +678,7 @@ namespace Ryujinx.Graphics.Vulkan
return; return;
} }
var program = _program; ShaderCollection program = _program;
if (_dirty.HasFlag(DirtyFlags.Uniform)) if (_dirty.HasFlag(DirtyFlags.Uniform))
{ {
@ -699,13 +699,19 @@ namespace Ryujinx.Graphics.Vulkan
if (_dirty.HasFlag(DirtyFlags.Texture)) if (_dirty.HasFlag(DirtyFlags.Texture))
{ {
if (program.UpdateTexturesWithoutTemplate) if (false)
{ {
UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp); UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp);
} }
else else
{ {
UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp); try {
UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
}
catch (Exception e)
{
UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp);
}
} }
} }
@ -757,18 +763,17 @@ namespace Ryujinx.Graphics.Vulkan
return mirrored; return mirrored;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateAndBind(CommandBufferScoped cbs, ShaderCollection program, int setIndex, PipelineBindPoint pbp) private void UpdateAndBind(CommandBufferScoped cbs, ShaderCollection program, int setIndex, PipelineBindPoint pbp)
{ {
var bindingSegments = program.BindingSegments[setIndex]; ResourceBindingSegment[] bindingSegments = program.BindingSegments[setIndex];
if (bindingSegments.Length == 0) if (bindingSegments.Length == 0)
{ {
return; return;
} }
var dummyBuffer = _dummyBuffer?.GetBuffer(); Auto<DisposableBuffer> dummyBuffer = _dummyBuffer?.GetBuffer();
if (_updateDescriptorCacheCbIndex) if (_updateDescriptorCacheCbIndex)
{ {
@ -776,7 +781,7 @@ namespace Ryujinx.Graphics.Vulkan
program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex); program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
} }
var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs); DescriptorSetCollection dsc = program.GetNewDescriptorSetCollection(setIndex, out bool isNew).Get(cbs);
if (!program.HasMinimalLayout) if (!program.HasMinimalLayout)
{ {
@ -811,12 +816,9 @@ namespace Ryujinx.Graphics.Vulkan
} }
} }
// Split buffer updates into individual slices for MoltenVK compatibility
ReadOnlySpan<DescriptorBufferInfo> uniformBuffers = _uniformBuffers; ReadOnlySpan<DescriptorBufferInfo> uniformBuffers = _uniformBuffers;
for (int i = 0; i < count; i++)
{ tu.Push(uniformBuffers.Slice(binding, count));
tu.Push(uniformBuffers.Slice(binding + i, 1));
}
} }
else if (setIndex == PipelineBase.StorageSetIndex) else if (setIndex == PipelineBase.StorageSetIndex)
{ {
@ -828,7 +830,7 @@ namespace Ryujinx.Graphics.Vulkan
if (_storageSet.Set(index)) if (_storageSet.Set(index))
{ {
ref var info = ref _storageBuffers[index]; ref DescriptorBufferInfo info = ref _storageBuffers[index];
bool mirrored = UpdateBuffer(cbs, bool mirrored = UpdateBuffer(cbs,
ref info, ref info,
@ -840,12 +842,9 @@ namespace Ryujinx.Graphics.Vulkan
} }
} }
// Split buffer updates into individual slices for MoltenVK compatibility
ReadOnlySpan<DescriptorBufferInfo> storageBuffers = _storageBuffers; ReadOnlySpan<DescriptorBufferInfo> storageBuffers = _storageBuffers;
for (int i = 0; i < count; i++)
{ tu.Push(storageBuffers.Slice(binding, count));
tu.Push(storageBuffers.Slice(binding + i, 1));
}
} }
else if (setIndex == PipelineBase.TextureSetIndex) else if (setIndex == PipelineBase.TextureSetIndex)
{ {
@ -857,8 +856,8 @@ namespace Ryujinx.Graphics.Vulkan
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
ref var texture = ref textures[i]; ref DescriptorImageInfo texture = ref textures[i];
ref var refs = ref _textureRefs[binding + i]; ref TextureRef refs = ref _textureRefs[binding + i];
texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default; texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default;
texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default; texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
@ -874,10 +873,7 @@ namespace Ryujinx.Graphics.Vulkan
} }
} }
for (int i = 0; i < count; i++) tu.Push<DescriptorImageInfo>(textures[..count]);
{
tu.Push<DescriptorImageInfo>(textures.Slice(i, 1));
}
} }
else else
{ {
@ -888,10 +884,7 @@ namespace Ryujinx.Graphics.Vulkan
bufferTextures[i] = _bufferTextureRefs[binding + i]?.GetBufferView(cbs, false) ?? default; bufferTextures[i] = _bufferTextureRefs[binding + i]?.GetBufferView(cbs, false) ?? default;
} }
for (int i = 0; i < count; i++) tu.Push<BufferView>(bufferTextures[..count]);
{
tu.Push<BufferView>(bufferTextures.Slice(i, 1));
}
} }
} }
else else
@ -919,10 +912,7 @@ namespace Ryujinx.Graphics.Vulkan
images[i].ImageView = _imageRefs[binding + i].ImageView?.Get(cbs).Value ?? default; images[i].ImageView = _imageRefs[binding + i].ImageView?.Get(cbs).Value ?? default;
} }
for (int i = 0; i < count; i++) tu.Push<DescriptorImageInfo>(images[..count]);
{
tu.Push<DescriptorImageInfo>(images.Slice(i, 1));
}
} }
else else
{ {
@ -933,10 +923,7 @@ namespace Ryujinx.Graphics.Vulkan
bufferImages[i] = _bufferImageRefs[binding + i]?.GetBufferView(cbs, true) ?? default; bufferImages[i] = _bufferImageRefs[binding + i]?.GetBufferView(cbs, true) ?? default;
} }
for (int i = 0; i < count; i++) tu.Push<BufferView>(bufferImages[..count]);
{
tu.Push<BufferView>(bufferImages.Slice(i, 1));
}
} }
} }
else else
@ -953,31 +940,29 @@ namespace Ryujinx.Graphics.Vulkan
} }
} }
var sets = dsc.GetSets(); DescriptorSet[] sets = dsc.GetSets();
_templateUpdater.Commit(_gd, _device, sets[0]); _templateUpdater.Commit(_gd, _device, sets[0]);
_gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan<uint>.Empty); _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan<uint>.Empty);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateAndBindTexturesWithoutTemplate(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp) private void UpdateAndBindTexturesWithoutTemplate(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp)
{ {
int setIndex = PipelineBase.TextureSetIndex; int setIndex = PipelineBase.TextureSetIndex;
var bindingSegments = program.BindingSegments[setIndex]; ResourceBindingSegment[] bindingSegments = program.BindingSegments[setIndex];
if (bindingSegments.Length == 0) if (bindingSegments.Length == 0)
{ {
return; return;
} }
var dummyImageInfo = new DescriptorImageInfo if (_updateDescriptorCacheCbIndex)
{ {
ImageView = _dummyTexture.GetImageView().Get(cbs).Value, _updateDescriptorCacheCbIndex = false;
Sampler = _dummySampler.GetSampler().Get(cbs).Value, program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
ImageLayout = ImageLayout.General }
};
var dsc = program.GetNewDescriptorSetCollection(setIndex, out _).Get(cbs); DescriptorSetCollection dsc = program.GetNewDescriptorSetCollection(setIndex, out _).Get(cbs);
foreach (ResourceBindingSegment segment in bindingSegments) foreach (ResourceBindingSegment segment in bindingSegments)
{ {
@ -988,78 +973,56 @@ namespace Ryujinx.Graphics.Vulkan
{ {
if (segment.Type != ResourceType.BufferTexture) if (segment.Type != ResourceType.BufferTexture)
{ {
Span<DescriptorImageInfo> textures = _textures;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
int index = binding + i; ref DescriptorImageInfo texture = ref textures[i];
ref var textureRef = ref _textureRefs[index]; ref TextureRef refs = ref _textureRefs[binding + i];
var imageView = textureRef.ImageView?.Get(cbs).Value ?? dummyImageInfo.ImageView; texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default;
var sampler = textureRef.Sampler?.Get(cbs).Value ?? dummyImageInfo.Sampler; texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
var imageInfo = new DescriptorImageInfo if (texture.ImageView.Handle == 0)
{ {
ImageView = imageView.Handle != 0 ? imageView : dummyImageInfo.ImageView, texture.ImageView = _dummyTexture.GetImageView().Get(cbs).Value;
Sampler = sampler.Handle != 0 ? sampler : dummyImageInfo.Sampler, }
ImageLayout = ImageLayout.General
};
dsc.UpdateImages(0, index, new[] { imageInfo }, DescriptorType.CombinedImageSampler); if (texture.Sampler.Handle == 0)
{
texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value;
}
} }
dsc.UpdateImages(0, binding, textures[..count], DescriptorType.CombinedImageSampler);
} }
else else
{ {
Span<BufferView> bufferTextures = _bufferTextures;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
int index = binding + i; bufferTextures[i] = _bufferTextureRefs[binding + i]?.GetBufferView(cbs, false) ?? default;
var bufferView = _bufferTextureRefs[index]?.GetBufferView(cbs, false) ?? default;
dsc.UpdateBufferImages(0, index, new[] { bufferView }, DescriptorType.UniformTexelBuffer);
} }
dsc.UpdateBufferImages(0, binding, bufferTextures[..count], DescriptorType.UniformTexelBuffer);
} }
} }
else else
{ {
var arrayRef = _textureArrayRefs[binding];
if (segment.Type != ResourceType.BufferTexture) if (segment.Type != ResourceType.BufferTexture)
{ {
var imageInfos = arrayRef.Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler); dsc.UpdateImages(0, binding, _textureArrayRefs[binding].Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler), DescriptorType.CombinedImageSampler);
if (imageInfos != null)
{
for (int i = 0; i < imageInfos.Length && i < count; i++)
{
dsc.UpdateImages(0, binding + i, new[] { imageInfos[i] }, DescriptorType.CombinedImageSampler);
}
}
else
{
for (int i = 0; i < count; i++)
{
dsc.UpdateImages(0, binding + i, new[] { dummyImageInfo }, DescriptorType.CombinedImageSampler);
}
}
} }
else else
{ {
var bufferViews = arrayRef.Array.GetBufferViews(cbs); dsc.UpdateBufferImages(0, binding, _textureArrayRefs[binding].Array.GetBufferViews(cbs), DescriptorType.UniformTexelBuffer);
if (bufferViews != null)
{
for (int i = 0; i < bufferViews.Length && i < count; i++)
{
dsc.UpdateBufferImages(0, binding + i, new[] { bufferViews[i] }, DescriptorType.UniformTexelBuffer);
}
}
else
{
for (int i = 0; i < count; i++)
{
dsc.UpdateBufferImages(0, binding + i, new[] { default(BufferView) }, DescriptorType.UniformTexelBuffer);
}
}
} }
} }
} }
var sets = dsc.GetSets(); DescriptorSet[] sets = dsc.GetSets();
_gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan<uint>.Empty); _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan<uint>.Empty);
} }
@ -1067,8 +1030,8 @@ namespace Ryujinx.Graphics.Vulkan
private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs) private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs)
{ {
int sequence = _pdSequence; int sequence = _pdSequence;
var bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex]; ResourceBindingSegment[] bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex];
var dummyBuffer = _dummyBuffer?.GetBuffer(); Auto<DisposableBuffer> dummyBuffer = _dummyBuffer?.GetBuffer();
long updatedBindings = 0; long updatedBindings = 0;
DescriptorSetTemplateWriter writer = _templateUpdater.Begin(32 * Unsafe.SizeOf<DescriptorBufferInfo>()); DescriptorSetTemplateWriter writer = _templateUpdater.Begin(32 * Unsafe.SizeOf<DescriptorBufferInfo>());
@ -1115,12 +1078,12 @@ namespace Ryujinx.Graphics.Vulkan
private void Initialize(CommandBufferScoped cbs, int setIndex, DescriptorSetCollection dsc) private void Initialize(CommandBufferScoped cbs, int setIndex, DescriptorSetCollection dsc)
{ {
// We don't support clearing texture descriptors currently. // We don't support clearing texture descriptors currently.
if (setIndex != PipelineBase.UniformSetIndex && setIndex != PipelineBase.StorageSetIndex) if (setIndex is not PipelineBase.UniformSetIndex and not PipelineBase.StorageSetIndex)
{ {
return; return;
} }
var dummyBuffer = _dummyBuffer?.GetBuffer().Get(cbs).Value ?? default; Buffer dummyBuffer = _dummyBuffer?.GetBuffer().Get(cbs).Value ?? default;
foreach (ResourceBindingSegment segment in _program.ClearSegments[setIndex]) foreach (ResourceBindingSegment segment in _program.ClearSegments[setIndex])
{ {
@ -1132,7 +1095,7 @@ namespace Ryujinx.Graphics.Vulkan
{ {
for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < program.BindingSegments.Length; setIndex++) for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < program.BindingSegments.Length; setIndex++)
{ {
var bindingSegments = program.BindingSegments[setIndex]; ResourceBindingSegment[] bindingSegments = program.BindingSegments[setIndex];
if (bindingSegments.Length == 0) if (bindingSegments.Length == 0)
{ {
@ -1145,10 +1108,10 @@ namespace Ryujinx.Graphics.Vulkan
{ {
DescriptorSet[] sets = null; DescriptorSet[] sets = null;
if (segment.Type == ResourceType.Texture || if (segment.Type is ResourceType.Texture or
segment.Type == ResourceType.Sampler || ResourceType.Sampler or
segment.Type == ResourceType.TextureAndSampler || ResourceType.TextureAndSampler or
segment.Type == ResourceType.BufferTexture) ResourceType.BufferTexture)
{ {
sets = _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets( sets = _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets(
_device, _device,
@ -1159,7 +1122,7 @@ namespace Ryujinx.Graphics.Vulkan
_dummyTexture, _dummyTexture,
_dummySampler); _dummySampler);
} }
else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage) else if (segment.Type is ResourceType.Image or ResourceType.BufferImage)
{ {
sets = _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets( sets = _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets(
_device, _device,
@ -1230,4 +1193,4 @@ namespace Ryujinx.Graphics.Vulkan
Dispose(true); Dispose(true);
} }
} }
} }

View file

@ -186,20 +186,18 @@ namespace Ryujinx.Graphics.Vulkan
return sets; return sets;
} }
DescriptorSetTemplate template = program.Templates[setIndex]; var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs);
DescriptorSetTemplateWriter tu = templateUpdater.Begin(template);
if (!_isBuffer) if (!_isBuffer)
{ {
tu.Push(GetImageInfos(_gd, cbs, dummyTexture)); dsc.UpdateImages(0, 0, GetImageInfos(_gd, cbs, dummyTexture), DescriptorType.StorageImage);
} }
else else
{ {
tu.Push(GetBufferViews(cbs)); dsc.UpdateBufferImages(0, 0, GetBufferViews(cbs), DescriptorType.StorageTexelBuffer);
} }
templateUpdater.Commit(_gd, device, sets[0]); sets = dsc.GetSets();
return sets; return sets;
} }

View file

@ -785,7 +785,7 @@ namespace Ryujinx.Graphics.Vulkan
shaderSubgroupSize: (int)Capabilities.SubgroupSize, shaderSubgroupSize: (int)Capabilities.SubgroupSize,
storageBufferOffsetAlignment: (int)limits.MinStorageBufferOffsetAlignment, storageBufferOffsetAlignment: (int)limits.MinStorageBufferOffsetAlignment,
textureBufferOffsetAlignment: (int)limits.MinTexelBufferOffsetAlignment, textureBufferOffsetAlignment: (int)limits.MinTexelBufferOffsetAlignment,
gatherBiasPrecision: (int)Capabilities.SubTexelPrecisionBits, //IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0, gatherBiasPrecision: IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0,
maximumGpuMemory: GetTotalGPUMemory()); maximumGpuMemory: GetTotalGPUMemory());
} }

View file

@ -21,25 +21,25 @@ namespace Ryujinx.HLE
/// The virtual file system used by the FS service. /// The virtual file system used by the FS service.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly VirtualFileSystem VirtualFileSystem; public readonly VirtualFileSystem VirtualFileSystem;
/// <summary> /// <summary>
/// The manager for handling a LibHac Horizon instance. /// The manager for handling a LibHac Horizon instance.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly LibHacHorizonManager LibHacHorizonManager; public readonly LibHacHorizonManager LibHacHorizonManager;
/// <summary> /// <summary>
/// The account manager used by the account service. /// The account manager used by the account service.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly AccountManager AccountManager; public readonly AccountManager AccountManager;
/// <summary> /// <summary>
/// The content manager used by the NCM service. /// The content manager used by the NCM service.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly ContentManager ContentManager; public readonly ContentManager ContentManager;
/// <summary> /// <summary>
/// The persistent information between run for multi-application capabilities. /// The persistent information between run for multi-application capabilities.
@ -51,93 +51,93 @@ namespace Ryujinx.HLE
/// The GPU renderer to use for all GPU operations. /// The GPU renderer to use for all GPU operations.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly IRenderer GpuRenderer; public readonly IRenderer GpuRenderer;
/// <summary> /// <summary>
/// The audio device driver to use for all audio operations. /// The audio device driver to use for all audio operations.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly IHardwareDeviceDriver AudioDeviceDriver; public readonly IHardwareDeviceDriver AudioDeviceDriver;
/// <summary> /// <summary>
/// The handler for various UI related operations needed outside of HLE. /// The handler for various UI related operations needed outside of HLE.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly IHostUIHandler HostUIHandler; public readonly IHostUIHandler HostUIHandler;
/// <summary> /// <summary>
/// Control the memory configuration used by the emulation context. /// Control the memory configuration used by the emulation context.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly MemoryConfiguration MemoryConfiguration; public readonly MemoryConfiguration MemoryConfiguration;
/// <summary> /// <summary>
/// The system language to use in the settings service. /// The system language to use in the settings service.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly SystemLanguage SystemLanguage; public readonly SystemLanguage SystemLanguage;
/// <summary> /// <summary>
/// The system region to use in the settings service. /// The system region to use in the settings service.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly RegionCode Region; public readonly RegionCode Region;
/// <summary> /// <summary>
/// Control the initial state of the vertical sync in the SurfaceFlinger service. /// Control the initial state of the vertical sync in the SurfaceFlinger service.
/// </summary> /// </summary>
internal readonly bool EnableVsync; public readonly bool EnableVsync;
/// <summary> /// <summary>
/// Control the initial state of the docked mode. /// Control the initial state of the docked mode.
/// </summary> /// </summary>
internal readonly bool EnableDockedMode; public readonly bool EnableDockedMode;
/// <summary> /// <summary>
/// Control if the Profiled Translation Cache (PTC) should be used. /// Control if the Profiled Translation Cache (PTC) should be used.
/// </summary> /// </summary>
internal readonly bool EnablePtc; public readonly bool EnablePtc;
/// <summary> /// <summary>
/// Control if the guest application should be told that there is a Internet connection available. /// Control if the guest application should be told that there is a Internet connection available.
/// </summary> /// </summary>
public bool EnableInternetAccess { internal get; set; } public bool EnableInternetAccess;
/// <summary> /// <summary>
/// Control LibHac's integrity check level. /// Control LibHac's integrity check level.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly IntegrityCheckLevel FsIntegrityCheckLevel; public readonly IntegrityCheckLevel FsIntegrityCheckLevel;
/// <summary> /// <summary>
/// Control LibHac's global access logging level. Value must be between 0 and 3. /// Control LibHac's global access logging level. Value must be between 0 and 3.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly int FsGlobalAccessLogMode; public readonly int FsGlobalAccessLogMode;
/// <summary> /// <summary>
/// The system time offset to apply to the time service steady and local clocks. /// The system time offset to apply to the time service steady and local clocks.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly long SystemTimeOffset; public readonly long SystemTimeOffset;
/// <summary> /// <summary>
/// The system timezone used by the time service. /// The system timezone used by the time service.
/// </summary> /// </summary>
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks> /// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
internal readonly string TimeZone; public readonly string TimeZone;
/// <summary> /// <summary>
/// Type of the memory manager used on CPU emulation. /// Type of the memory manager used on CPU emulation.
/// </summary> /// </summary>
public MemoryManagerMode MemoryManagerMode { internal get; set; } public MemoryManagerMode MemoryManagerMode { get; set; }
/// <summary> /// <summary>
/// Control the initial state of the ignore missing services setting. /// Control the initial state of the ignore missing services setting.
/// If this is set to true, when a missing service is encountered, it will try to automatically handle it instead of throwing an exception. /// If this is set to true, when a missing service is encountered, it will try to automatically handle it instead of throwing an exception.
/// </summary> /// </summary>
/// TODO: Update this again. /// TODO: Update this again.
public bool IgnoreMissingServices { internal get; set; } public bool IgnoreMissingServices { get; set; }
/// <summary> /// <summary>
/// Aspect Ratio applied to the renderer window by the SurfaceFlinger service. /// Aspect Ratio applied to the renderer window by the SurfaceFlinger service.
@ -152,22 +152,22 @@ namespace Ryujinx.HLE
/// <summary> /// <summary>
/// Use Hypervisor over JIT if available. /// Use Hypervisor over JIT if available.
/// </summary> /// </summary>
internal readonly bool UseHypervisor; public readonly bool UseHypervisor;
/// <summary> /// <summary>
/// Multiplayer LAN Interface ID (device GUID) /// Multiplayer LAN Interface ID (device GUID)
/// </summary> /// </summary>
public string MultiplayerLanInterfaceId { internal get; set; } public string MultiplayerLanInterfaceId { get; set; }
/// <summary> /// <summary>
/// Multiplayer Mode /// Multiplayer Mode
/// </summary> /// </summary>
public MultiplayerMode MultiplayerMode { internal get; set; } public MultiplayerMode MultiplayerMode { get; set; }
/// <summary> /// <summary>
/// An action called when HLE force a refresh of output after docked mode changed. /// An action called when HLE force a refresh of output after docked mode changed.
/// </summary> /// </summary>
public Action RefreshInputConfig { internal get; set; } public Action RefreshInputConfig { get; set; }
public HLEConfiguration(VirtualFileSystem virtualFileSystem, public HLEConfiguration(VirtualFileSystem virtualFileSystem,
LibHacHorizonManager libHacHorizonManager, LibHacHorizonManager libHacHorizonManager,

View file

@ -396,7 +396,7 @@ namespace Ryujinx.Headless.SDL2
[UnmanagedCallersOnly(EntryPoint = "pause_emulation")] [UnmanagedCallersOnly(EntryPoint = "pause_emulation")]
public static void PauseEmulation(bool shouldPause) public static void PauseEmulation(bool shouldPause)
{ {
if (_window != null) if (_window != null && _window.Device != null)
{ {
if (!shouldPause) if (!shouldPause)
{ {
@ -1721,5 +1721,137 @@ namespace Ryujinx.Headless.SDL2
span.Clear(); span.Clear();
Encoding.UTF8.GetBytes(source, span); Encoding.UTF8.GetBytes(source, span);
} }
[UnmanagedCallersOnly(EntryPoint = "update_settings_external")]
public static unsafe int UpdateSettingsExternal(int argCount, IntPtr* pArgs)
{
string[] args = new string[argCount];
try
{
for (int i = 0; i < argCount; i++)
{
args[i] = Marshal.PtrToStringAnsi(pArgs[i]);
}
Options parsedOptions = null;
Parser.Default.ParseArguments<Options>(args)
.WithParsed(opts => parsedOptions = opts);
if (parsedOptions == null)
{
Console.WriteLine("Failed to parse options.");
return -1;
}
ApplyDynamicSettings(parsedOptions);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
return -1;
}
return 0;
}
private static void ApplyDynamicSettings(Options options)
{
Graphics.Gpu.GraphicsConfig.ResScale = options.ResScale;
Graphics.Gpu.GraphicsConfig.MaxAnisotropy = options.MaxAnisotropy;
Graphics.Gpu.GraphicsConfig.EnableShaderCache = !options.DisableShaderCache;
Graphics.Gpu.GraphicsConfig.EnableTextureRecompression = options.EnableTextureRecompression;
Graphics.Gpu.GraphicsConfig.EnableMacroHLE = !options.DisableMacroHLE;
if (_window != null)
{
_window.IsFullscreen = options.IsFullscreen;
_window.DisplayId = options.DisplayId;
_window.IsExclusiveFullscreen = options.IsExclusiveFullscreen;
_window.ExclusiveFullscreenWidth = options.ExclusiveFullscreenWidth;
_window.ExclusiveFullscreenHeight = options.ExclusiveFullscreenHeight;
_window.AntiAliasing = options.AntiAliasing;
_window.ScalingFilter = options.ScalingFilter;
_window.ScalingFilterLevel = options.ScalingFilterLevel;
_window._aspectRatio = options.AspectRatio;
}
if (_emulationContext != null)
{
_emulationContext.SetVolume(options.AudioVolume);
_emulationContext.System.State.SetLanguage(options.SystemLanguage);
_emulationContext.System.State.SetRegion(options.SystemRegion);
_emulationContext.EnableDeviceVsync = !options.DisableVSync;
_emulationContext.System.State.DockedMode = !options.DisableDockedMode;
_emulationContext.System.EnablePtc = !options.DisablePTC;
_emulationContext.System.FsIntegrityCheckLevel = !options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None;
_emulationContext.System.GlobalAccessLogMode = options.FsGlobalAccessLogMode;
_emulationContext.Configuration.IgnoreMissingServices = options.IgnoreMissingServices;
_emulationContext.Configuration.AspectRatio = options.AspectRatio;
_emulationContext.Configuration.EnableInternetAccess = options.EnableInternetAccess;
_emulationContext.Configuration.MemoryManagerMode = options.MemoryManagerMode;
_emulationContext.Configuration.MultiplayerLanInterfaceId = options.MultiplayerLanInterfaceId;
}
Logger.SetEnable(LogLevel.Debug, options.LoggingEnableDebug);
Logger.SetEnable(LogLevel.Stub, !options.LoggingDisableStub);
Logger.SetEnable(LogLevel.Info, !options.LoggingDisableInfo);
Logger.SetEnable(LogLevel.Warning, !options.LoggingDisableWarning);
Logger.SetEnable(LogLevel.Error, options.LoggingEnableError);
Logger.SetEnable(LogLevel.Trace, options.LoggingEnableTrace);
Logger.SetEnable(LogLevel.Guest, !options.LoggingDisableGuest);
Logger.SetEnable(LogLevel.AccessLog, options.LoggingEnableFsAccessLog);
}
// Old :3
private static void ReplaceEmulationContextConfiguration(Switch emu, Options options)
{
var oldConfig = emu.Configuration;
var newConfig = new HLEConfiguration(
_virtualFileSystem,
_libHacHorizonManager,
_contentManager,
_accountManager,
_userChannelPersistence,
oldConfig.GpuRenderer,
oldConfig.AudioDeviceDriver,
oldConfig.MemoryConfiguration,
oldConfig.HostUIHandler,
options.SystemLanguage,
options.SystemRegion,
!options.DisableVSync,
!options.DisableDockedMode,
!options.DisablePTC,
options.EnableInternetAccess,
!options.DisableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None,
options.FsGlobalAccessLogMode,
options.SystemTimeOffset,
options.SystemTimeZone,
options.MemoryManagerMode,
options.IgnoreMissingServices,
options.AspectRatio,
options.AudioVolume,
options.UseHypervisor,
options.MultiplayerLanInterfaceId,
Ryujinx.Common.Configuration.Multiplayer.MultiplayerMode.LdnMitm
);
var configField = typeof(Switch).GetField("<Configuration>k__BackingField", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (configField != null)
{
configField.SetValue(emu, newConfig);
}
emu.System.State.SetLanguage(newConfig.SystemLanguage);
emu.System.State.SetRegion(newConfig.Region);
emu.EnableDeviceVsync = newConfig.EnableVsync;
emu.System.State.DockedMode = newConfig.EnableDockedMode;
emu.System.EnablePtc = newConfig.EnablePtc;
emu.System.FsIntegrityCheckLevel = newConfig.FsIntegrityCheckLevel;
emu.System.GlobalAccessLogMode = newConfig.FsGlobalAccessLogMode;
}
} }
} }

View file

@ -85,7 +85,7 @@ namespace Ryujinx.Headless.SDL2
private string _gpuDriverName; private string _gpuDriverName;
private readonly AspectRatio _aspectRatio; public AspectRatio _aspectRatio;
private readonly bool _enableMouse; private readonly bool _enableMouse;
public WindowBase( public WindowBase(
@ -162,7 +162,7 @@ namespace Ryujinx.Headless.SDL2
private void InitializeWindow() private void InitializeWindow()
{ {
if (this is Ryujinx.Headless.SDL2.Vulkan.MoltenVKWindow) { if (this is Vulkan.MoltenVKWindow) {
string message = $"Not using SDL Windows, Skipping..."; string message = $"Not using SDL Windows, Skipping...";
Logger.Info?.Print(LogClass.Application, message); Logger.Info?.Print(LogClass.Application, message);