App Icon Switcher, Per-App Settings Page, Update Virtual Controller and more
|
@ -75,7 +75,7 @@ namespace ARMeilleure.Translation
|
|||
FunctionTable = new AddressTable<ulong>(for64Bits ? _levels64Bit : _levels32Bit);
|
||||
Stubs = new TranslatorStubs(FunctionTable);
|
||||
|
||||
FunctionTable.Fill = (ulong)Stubs.DispatchStub;
|
||||
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
|
||||
}
|
||||
|
||||
public IPtcLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled)
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
VERSION = 1.7.0
|
||||
VERSION = 2.0
|
||||
|
|
|
@ -32,13 +32,6 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
4E59B0A32DEA5CA9004BFF2A /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
|
||||
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
|
||||
};
|
||||
4E80A99E2CD6F54700029585 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||
|
@ -53,6 +46,13 @@
|
|||
remoteGlobalIDString = 4E80A98C2CD6F54500029585;
|
||||
remoteInfo = MeloNX;
|
||||
};
|
||||
4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = BD43C6212D1B248D003BBC42;
|
||||
remoteInfo = com.Stossy11.MeloNX.RyujinxAg;
|
||||
};
|
||||
BD43C6252D1B249E003BBC42 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 4E80A9852CD6F54500029585 /* Project object */;
|
||||
|
@ -294,7 +294,7 @@
|
|||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
4E59B0A42DEA5CA9004BFF2A /* PBXTargetDependency */,
|
||||
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
4E80A98F2CD6F54500029585 /* MeloNX */,
|
||||
|
@ -482,11 +482,6 @@
|
|||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
4E59B0A42DEA5CA9004BFF2A /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
|
||||
targetProxy = 4E59B0A32DEA5CA9004BFF2A /* PBXContainerItemProxy */;
|
||||
};
|
||||
4E80A99F2CD6F54700029585 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
||||
|
@ -497,6 +492,11 @@
|
|||
target = 4E80A98C2CD6F54500029585 /* MeloNX */;
|
||||
targetProxy = 4E80A9A82CD6F54700029585 /* PBXContainerItemProxy */;
|
||||
};
|
||||
4EFFCD192DFB766F00F78EA6 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BD43C6212D1B248D003BBC42 /* com.Stossy11.MeloNX.RyujinxAg */;
|
||||
targetProxy = 4EFFCD182DFB766F00F78EA6 /* PBXContainerItemProxy */;
|
||||
};
|
||||
BD43C6262D1B249E003BBC42 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = BD43C61D2D1B23AB003BBC42 /* Ryujinx */;
|
||||
|
@ -647,13 +647,16 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
||||
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_TESTABILITY = NO;
|
||||
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",
|
||||
);
|
||||
GCC_OPTIMIZATION_LEVEL = z;
|
||||
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",
|
||||
);
|
||||
MARKETING_VERSION = "$(VERSION)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
|
@ -1025,13 +1056,16 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4E12B23B2D797CFA00FB2271 /* MeloNX.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = PixelAppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = MeloNX/MeloNX.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = 95J8WZ4TN8;
|
||||
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
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",
|
||||
);
|
||||
GCC_OPTIMIZATION_LEVEL = z;
|
||||
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",
|
||||
);
|
||||
MARKETING_VERSION = "$(VERSION)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
|
|
|
@ -59,6 +59,8 @@ void initialize();
|
|||
|
||||
int main_ryujinx_sdl(int argc, char **argv);
|
||||
|
||||
int update_settings_external(int argc, char **argv);
|
||||
|
||||
int get_current_fps();
|
||||
|
||||
void touch_began(float x, float y, int index);
|
||||
|
|
|
@ -9,8 +9,6 @@ import Foundation
|
|||
import Network
|
||||
import UIKit
|
||||
|
||||
|
||||
|
||||
func stikJITorStikDebug() -> Int {
|
||||
let teamid = SecTaskCopyTeamIdentifier(SecTaskCreateFromSelf(nil)!, nil)
|
||||
|
||||
|
@ -25,15 +23,28 @@ func stikJITorStikDebug() -> Int {
|
|||
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 {
|
||||
|
||||
guard let handle = dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices", RTLD_LAZY) else {
|
||||
if let error = dlerror() {
|
||||
print(String(cString: error))
|
||||
}
|
||||
return false
|
||||
// fatalError("Failed to open dylib")
|
||||
}
|
||||
|
||||
typealias SBSLaunchApplicationWithIdentifierFunc = @convention(c) (CFString, Bool) -> Int32
|
||||
|
|
|
@ -356,7 +356,9 @@ class Ryujinx : ObservableObject {
|
|||
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
|
||||
|
||||
if result != 0 {
|
||||
self.isRunning = false
|
||||
DispatchQueue.main.async {
|
||||
self.isRunning = false
|
||||
}
|
||||
if let accessing, accessing {
|
||||
url!.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
@ -365,7 +367,9 @@ class Ryujinx : ObservableObject {
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
self.isRunning = false
|
||||
DispatchQueue.main.async {
|
||||
self.isRunning = false
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 0.3)
|
||||
let logs = LogCapture.shared.capturedLogs
|
||||
let parsedLogs = extractExceptionInfo(logs)
|
||||
|
@ -384,14 +388,19 @@ class Ryujinx : ObservableObject {
|
|||
|
||||
|
||||
presentAlert(title: "MeloNX Crashed!", message: parsedLogs.exceptionType + ": " + parsedLogs.message) {
|
||||
|
||||
assert(true, parsedLogs.exceptionType)
|
||||
UIApplication.shared.perform(#selector(NSXPCConnection.suspend))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
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] = []
|
||||
|
||||
// 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)
|
||||
if !config.inputDSUServers.isEmpty {
|
||||
config.inputDSUServers.prefix(8).enumerated().forEach { index, inputDSUServer in
|
||||
if config.handHeldController {
|
||||
args.append(contentsOf: ["\(index == 0 ? "--input-dsu-server-handheld" : "--input-dsu-server-\(index + 1)")", inputDSUServer])
|
||||
} else {
|
||||
args.append(contentsOf: ["--input-dsu-server-\(index + 1)", inputDSUServer])
|
||||
if index == 0 {
|
||||
args.append(contentsOf: ["--input-dsu-server-handheld", inputDSUServer])
|
||||
}
|
||||
args.append(contentsOf: ["--input-dsu-server-\(index + 1)", inputDSUServer])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -264,6 +264,7 @@ struct ABXYView: View {
|
|||
|
||||
struct ButtonView: View {
|
||||
var button: VirtualControllerButton
|
||||
var callback: (() -> Void)? = nil
|
||||
|
||||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
|
@ -344,23 +345,29 @@ struct ButtonView: View {
|
|||
}
|
||||
|
||||
private func handleButtonPress() {
|
||||
guard !isPressed || istoggle else { return }
|
||||
|
||||
if istoggle {
|
||||
toggleState.toggle()
|
||||
isPressed = toggleState
|
||||
let value = toggleState ? 1 : 0
|
||||
Ryujinx.shared.virtualController.setButtonState(Uint8(value), for: button)
|
||||
Haptics.shared.play(.medium)
|
||||
if let callback {
|
||||
callback()
|
||||
} else {
|
||||
isPressed = true
|
||||
Ryujinx.shared.virtualController.setButtonState(1, for: button)
|
||||
Haptics.shared.play(.medium)
|
||||
guard !isPressed || istoggle else { return }
|
||||
|
||||
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() {
|
||||
if istoggle { return }
|
||||
|
||||
if let callback { return }
|
||||
|
||||
guard isPressed else { return }
|
||||
|
||||
|
@ -397,40 +404,23 @@ struct ButtonView: View {
|
|||
// Centralized button configuration
|
||||
private var buttonConfig: ButtonConfiguration {
|
||||
switch button {
|
||||
case .A:
|
||||
return ButtonConfiguration(iconName: "a.circle.fill")
|
||||
case .B:
|
||||
return ButtonConfiguration(iconName: "b.circle.fill")
|
||||
case .X:
|
||||
return ButtonConfiguration(iconName: "x.circle.fill")
|
||||
case .Y:
|
||||
return ButtonConfiguration(iconName: "y.circle.fill")
|
||||
case .leftStick:
|
||||
return ButtonConfiguration(iconName: "l.joystick.press.down.fill")
|
||||
case .rightStick:
|
||||
return ButtonConfiguration(iconName: "r.joystick.press.down.fill")
|
||||
case .dPadUp:
|
||||
return ButtonConfiguration(iconName: "arrowtriangle.up.circle.fill")
|
||||
case .dPadDown:
|
||||
return ButtonConfiguration(iconName: "arrowtriangle.down.circle.fill")
|
||||
case .dPadLeft:
|
||||
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")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -438,3 +428,121 @@ struct ButtonView: View {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ struct Joystick: View {
|
|||
@State private var offset: CGSize = .zero
|
||||
@Binding var showBackground: Bool
|
||||
|
||||
let sensitivity: CGFloat = 1.5
|
||||
let sensitivity: CGFloat = 1.2
|
||||
|
||||
|
||||
var dragGesture: some Gesture {
|
||||
|
|
|
@ -28,7 +28,6 @@ struct JoystickController: View {
|
|||
VStack {
|
||||
Joystick(position: $position, joystickSize: dragDiameter * 0.2, boundarySize: dragDiameter, showBackground: $showBackground)
|
||||
.onChange(of: position) { newValue in
|
||||
|
||||
if iscool != nil {
|
||||
Ryujinx.shared.virtualController.thumbstickMoved(.right, x: newValue.x, y: newValue.y)
|
||||
} else {
|
||||
|
|
|
@ -24,6 +24,8 @@ struct EmulationView: View {
|
|||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State private var isInBackground = false
|
||||
@State var showSettings = false
|
||||
@State var pauseEmu = true
|
||||
@AppStorage("location-enabled") var locationenabled: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
@ -80,15 +82,47 @@ struct EmulationView: View {
|
|||
if ssb {
|
||||
HStack {
|
||||
|
||||
Image(systemName: "arrow.left.circle")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.onTapGesture {
|
||||
Menu {
|
||||
|
||||
/*
|
||||
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
|
||||
stop_emulation()
|
||||
try? Ryujinx.shared.stop()
|
||||
} label: {
|
||||
Label {
|
||||
Text("Exit (Unstable)")
|
||||
} icon: {
|
||||
Image(systemName: "x.circle")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
} label: {
|
||||
ExtButtonIconView(button: .guide, opacity: 0.4)
|
||||
}
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
|
||||
|
@ -122,5 +156,11 @@ struct EmulationView: View {
|
|||
isInBackground = true
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
// PerGameSettingsView(titleId: startgame?.titleId ?? "", manager: InGameSettingsManager.shared)
|
||||
// .onDisappear() {
|
||||
// InGameSettingsManager.shared.saveSettings()
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() ?? [:]
|
||||
}
|
||||
}
|
|
@ -357,8 +357,16 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@StateObject private var persettings = PerGameSettingsManager.shared
|
||||
private func start(displayid: UInt32) {
|
||||
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.inputids = Array(Set(currentControllers.map(\.id)))
|
||||
|
@ -367,9 +375,7 @@ struct ContentView: View {
|
|||
|
||||
registerMotionForMatchingControllers()
|
||||
|
||||
if config.inputids.isEmpty {
|
||||
config.inputids.append("0")
|
||||
}
|
||||
config.inputids.isEmpty ? config.inputids.append("0") : ()
|
||||
|
||||
// Local DSU loopback to ryujinx per input id
|
||||
for _ in config.inputids {
|
||||
|
|
|
@ -26,6 +26,15 @@ struct GameLibraryView: View {
|
|||
@State var startgame = false
|
||||
@State var isSelectingGameFile = 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 isSelectingGameDLC: Bool = false
|
||||
@StateObject var ryujinx = Ryujinx.shared
|
||||
|
@ -201,6 +210,9 @@ struct GameLibraryView: View {
|
|||
.sheet(isPresented: $isSelectingGameDLC) {
|
||||
DLCManagerSheet(game: $gameInfo)
|
||||
}
|
||||
.sheet(isPresented: isShowingPerGameSettings) {
|
||||
PerGameSettingsView(titleId: gamePerGameSettings!.titleId)
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { isViewingGameInfo && gameInfo != nil },
|
||||
set: { newValue in
|
||||
|
@ -271,7 +283,8 @@ struct GameLibraryView: View {
|
|||
isSelectingGameUpdate: $isSelectingGameUpdate,
|
||||
isSelectingGameDLC: $isSelectingGameDLC,
|
||||
gameRequirements: $gameRequirements,
|
||||
gameInfo: $gameInfo
|
||||
gameInfo: $gameInfo,
|
||||
perGameSettings: $gamePerGameSettings
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
|
@ -288,7 +301,8 @@ struct GameLibraryView: View {
|
|||
isSelectingGameUpdate: $isSelectingGameUpdate,
|
||||
isSelectingGameDLC: $isSelectingGameDLC,
|
||||
gameRequirements: $gameRequirements,
|
||||
gameInfo: $gameInfo
|
||||
gameInfo: $gameInfo,
|
||||
perGameSettings: $gamePerGameSettings
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
|
@ -482,6 +496,12 @@ struct GameLibraryView: View {
|
|||
} label: {
|
||||
Label("Game Info", systemImage: "info.circle")
|
||||
}
|
||||
|
||||
Button {
|
||||
gamePerGameSettings = game
|
||||
} label: {
|
||||
Label("\(game.titleName) Settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
|
@ -501,6 +521,12 @@ struct GameLibraryView: View {
|
|||
}
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
removeFromRecentGames(game)
|
||||
} label: {
|
||||
Label("Remove from Recents", systemImage: "trash")
|
||||
}
|
||||
|
||||
if #available(iOS 15, *) {
|
||||
Button(role: .destructive) {
|
||||
deleteGame(game: game)
|
||||
|
@ -771,6 +797,8 @@ struct GameListRow: View {
|
|||
@Binding var isSelectingGameDLC: Bool
|
||||
@Binding var gameRequirements: [GameRequirements]
|
||||
@Binding var gameInfo: Game?
|
||||
@StateObject private var settingsManager = PerGameSettingsManager.shared
|
||||
@Binding var perGameSettings: Game?
|
||||
@State var gametoDelete: Game?
|
||||
@State var showGameDeleteConfirmation: Bool = false
|
||||
@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()
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -897,6 +933,12 @@ struct GameListRow: View {
|
|||
} label: {
|
||||
Label("Game Info", systemImage: "info.circle")
|
||||
}
|
||||
|
||||
Button {
|
||||
perGameSettings = game
|
||||
} label: {
|
||||
Label("\(game.titleName) Settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
|
@ -959,10 +1001,7 @@ struct GameListRow: View {
|
|||
Text("Are you sure you want to delete \(gametoDelete?.titleName ?? "this game")?")
|
||||
}
|
||||
.listRowInsets(EdgeInsets())
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(colorScheme == .dark ? Color(.systemGray6) : Color(.systemGray6).opacity(0.5))
|
||||
)
|
||||
.wow(colorScheme)
|
||||
} else {
|
||||
Button(action: {
|
||||
startemu = game
|
||||
|
@ -1196,3 +1235,20 @@ func pullGameCompatibility(completion: @escaping (Result<[GameRequirements], Err
|
|||
|
||||
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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -266,6 +266,8 @@ struct SettingsViewNew: View {
|
|||
|
||||
@AppStorage("runOnMainThread") var runOnMainThread = false
|
||||
|
||||
@AppStorage("oldSettingsUI") var oldSettingsUI = false
|
||||
|
||||
@AppCodableStorage("toggleButtons") var toggleButtons = ToggleButtonsState()
|
||||
|
||||
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
||||
|
@ -275,6 +277,7 @@ struct SettingsViewNew: View {
|
|||
@State private var showResolutionInfo = false
|
||||
@State private var showAnisotropicInfo = false
|
||||
@State private var showControllerInfo = false
|
||||
@State private var showAppIconSwitcher = false
|
||||
@State private var searchText = ""
|
||||
@AppStorage("portal") var gamepo = false
|
||||
@StateObject var ryujinx = Ryujinx.shared
|
||||
|
@ -327,10 +330,12 @@ struct SettingsViewNew: View {
|
|||
var body: some View {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
iOSSettings
|
||||
} else {
|
||||
} else if !oldSettingsUI {
|
||||
iPadOSSettings
|
||||
.ignoresSafeArea()
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
} else {
|
||||
iOSSettings
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1188,7 +1193,6 @@ struct SettingsViewNew: View {
|
|||
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
|
||||
TextField("Separate arguments with commas", text: binding)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
|
@ -1254,13 +1258,31 @@ struct SettingsViewNew: View {
|
|||
Divider()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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
|
||||
SettingsToggle(isOn: $ssb, icon: "arrow.left.circle", label: "Exit Button")
|
||||
SettingsToggle(isOn: $ssb, icon: "arrow.left.circle", label: "Menu Button (in-game)")
|
||||
|
||||
Divider()
|
||||
|
||||
|
@ -1275,6 +1297,13 @@ struct SettingsViewNew: View {
|
|||
|
||||
Divider()
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
// Old Settings UI
|
||||
SettingsToggle(isOn: $oldSettingsUI, icon: "ipad.landscape", label: "Non Switch-like Settings")
|
||||
|
||||
Divider()
|
||||
}
|
||||
|
||||
|
||||
// JIT options
|
||||
if #available(iOS 17.0.1, *) {
|
||||
|
@ -1401,8 +1430,6 @@ struct SVGView: UIViewRepresentable {
|
|||
svgName.removeLast(4)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_ = UIView(svgNamed: svgName) { svgLayer in
|
||||
svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
|
||||
svgLayer.resizeToFit(hammock.frame)
|
||||
|
@ -1505,6 +1532,7 @@ struct SettingsSection<Content: View>: View {
|
|||
|
||||
struct SettingsCard<Content: View>: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@AppStorage("oldSettingsUI") var oldSettingsUI = false
|
||||
let content: Content
|
||||
|
||||
init(@ViewBuilder content: () -> Content) {
|
||||
|
@ -1512,7 +1540,7 @@ struct SettingsCard<Content: View>: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone || oldSettingsUI {
|
||||
content
|
||||
.padding()
|
||||
.background(
|
||||
|
@ -1538,9 +1566,10 @@ struct SettingsToggle: View {
|
|||
let label: String
|
||||
var disabled: Bool = false
|
||||
@AppStorage("toggleGreen") var toggleGreen: Bool = false
|
||||
@AppStorage("oldSettingsUI") var oldSettingsUI = false
|
||||
|
||||
var body: some View {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone || oldSettingsUI {
|
||||
Toggle(isOn: $isOn) {
|
||||
HStack(spacing: 8) {
|
||||
if icon.hasSuffix(".svg") {
|
||||
|
|
|
@ -40,40 +40,61 @@ struct MeloNXApp: App {
|
|||
|
||||
@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 {
|
||||
WindowGroup {
|
||||
if finishedStorage {
|
||||
ContentView()
|
||||
.withFileImporter()
|
||||
.onAppear {
|
||||
if checkForUpdate {
|
||||
checkLatestVersion()
|
||||
Group {
|
||||
if finishedStorage {
|
||||
ContentView()
|
||||
.withFileImporter()
|
||||
.onAppear {
|
||||
if checkForUpdate {
|
||||
checkLatestVersion()
|
||||
}
|
||||
|
||||
print(metalHudEnabler.canMetalHud)
|
||||
|
||||
UserDefaults.standard.set(false, forKey: "lockInApp")
|
||||
}
|
||||
|
||||
print(metalHudEnabler.canMetalHud)
|
||||
|
||||
UserDefaults.standard.set(false, forKey: "lockInApp")
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { showOutOfDateSheet && updateInfo != nil },
|
||||
set: { newValue in
|
||||
if !newValue {
|
||||
showOutOfDateSheet = false
|
||||
updateInfo = nil
|
||||
.sheet(isPresented: Binding(
|
||||
get: { showOutOfDateSheet && updateInfo != nil },
|
||||
set: { newValue in
|
||||
if !newValue {
|
||||
showOutOfDateSheet = false
|
||||
updateInfo = nil
|
||||
}
|
||||
}
|
||||
)) {
|
||||
if let updateInfo = updateInfo {
|
||||
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
|
||||
}
|
||||
}
|
||||
)) {
|
||||
if let updateInfo = updateInfo {
|
||||
MeloNXUpdateSheet(updateInfo: updateInfo, isPresented: $showOutOfDateSheet)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SetupView(finished: $finished)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
func changeAppUI(_ string: String) -> String? {
|
||||
guard let data = Data(base64Encoded: string) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 203 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/DarkMode.imageset/darker.png
vendored
Normal file
After Width: | Height: | Size: 203 KiB |
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 24 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelAppIcon.imageset/MeloNX 1024.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 24 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/PixelRoundAppIcon.imageset/PixelPomeloNX 1024.png
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 383 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/RoundAppIcon.imageset/copycat.png
vendored
Normal file
After Width: | Height: | Size: 383 KiB |
|
@ -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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 269 KiB |
21
src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
BIN
src/MeloNX/MeloNX/Assets/Assets.xcassets/uwuAppIcon.imageset/melowonx.png
vendored
Normal file
After Width: | Height: | Size: 269 KiB |
|
@ -10,7 +10,7 @@
|
|||
</data>
|
||||
<key>Info.plist</key>
|
||||
<data>
|
||||
UOH9NuuEcz5NQiQlrM2LNFaG2pI=
|
||||
RTwvCLsTMs+YfZ9ZeF25QYe7/LE=
|
||||
</data>
|
||||
<key>Modules/module.modulemap</key>
|
||||
<data>
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<dict>
|
||||
<key>files</key>
|
||||
<dict>
|
||||
<key>.DS_Store</key>
|
||||
<data>
|
||||
7Mfr8shT4pXWBr/plN+uNkIabdM=
|
||||
</data>
|
||||
<key>Headers/StosJIT-Swift.h</key>
|
||||
<data>
|
||||
h9vaTwhC6FlnyKmIkaxLQGlFd1g=
|
||||
|
@ -26,7 +30,7 @@
|
|||
</data>
|
||||
<key>Modules/StosJIT.swiftmodule/Project/arm64-apple-ios.swiftsourceinfo</key>
|
||||
<data>
|
||||
2mJoWBgg56N+3OxKfIDMLZFNHVk=
|
||||
nihJghwM5m7kxkQD7UvrWyHkLy8=
|
||||
</data>
|
||||
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key>
|
||||
<data>
|
||||
|
@ -79,7 +83,7 @@
|
|||
<dict>
|
||||
<key>hash2</key>
|
||||
<data>
|
||||
sZBe57nozztJzv83RPLjKIRYGSQmeE7XYCqr63xZONM=
|
||||
+Ehvco7cQbAaF7zufvBYTiGXFp37Hjym/Pav514sGPk=
|
||||
</data>
|
||||
</dict>
|
||||
<key>Modules/StosJIT.swiftmodule/arm64-apple-ios.abi.json</key>
|
||||
|
|
|
@ -159,7 +159,14 @@ namespace Ryujinx.Graphics.GAL.Multithreading
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,9 +31,16 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
_gd = gd;
|
||||
_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;
|
||||
|
||||
for (int seg = 0; seg < segments.Length; seg++)
|
||||
|
@ -42,45 +49,36 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
int binding = segment.Binding;
|
||||
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(),
|
||||
DstBinding = (uint)binding,
|
||||
DescriptorCount = (uint)count,
|
||||
stride = (nuint)Unsafe.SizeOf<DescriptorBufferInfo>();
|
||||
}
|
||||
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,
|
||||
Stride = (nuint)Unsafe.SizeOf<DescriptorBufferInfo>()
|
||||
Stride = stride
|
||||
};
|
||||
|
||||
structureOffset += (nuint)(Unsafe.SizeOf<DescriptorBufferInfo>() * count);
|
||||
}
|
||||
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);
|
||||
structureOffset += stride;
|
||||
entryIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,7 +87,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
var info = new DescriptorUpdateTemplateCreateInfo()
|
||||
{
|
||||
SType = StructureType.DescriptorUpdateTemplateCreateInfo,
|
||||
DescriptorUpdateEntryCount = (uint)segments.Length,
|
||||
DescriptorUpdateEntryCount = (uint)totalDescriptors,
|
||||
PDescriptorUpdateEntries = entries,
|
||||
|
||||
TemplateType = DescriptorUpdateTemplateType.DescriptorSet,
|
||||
|
@ -124,23 +122,6 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
int entry = 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)
|
||||
{
|
||||
for (int i = 0; i < descriptor.Count; i++)
|
||||
|
@ -149,28 +130,21 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
if (bindingCount == 0)
|
||||
{
|
||||
startBinding = binding;
|
||||
}
|
||||
|
||||
bindingCount++;
|
||||
structureOffset += (nuint)Unsafe.SizeOf<DescriptorBufferInfo>();
|
||||
entry++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bindingCount > 0)
|
||||
{
|
||||
AddBinding(startBinding, bindingCount);
|
||||
}
|
||||
|
||||
Size = (int)structureOffset;
|
||||
|
||||
var info = new DescriptorUpdateTemplateCreateInfo()
|
||||
|
|
|
@ -3,10 +3,10 @@ using Ryujinx.Graphics.GAL;
|
|||
using Ryujinx.Graphics.Shader;
|
||||
using Silk.NET.Vulkan;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Buffer = Silk.NET.Vulkan.Buffer;
|
||||
using CompareOp = Ryujinx.Graphics.GAL.CompareOp;
|
||||
using Format = Ryujinx.Graphics.GAL.Format;
|
||||
using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
|
||||
|
@ -141,11 +141,11 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
_bufferTextureRefs = new TextureBuffer[Constants.MaxTextureBindings * 2];
|
||||
_bufferImageRefs = new TextureBuffer[Constants.MaxImageBindings * 2];
|
||||
|
||||
_textureArrayRefs = Array.Empty<ArrayRef<TextureArray>>();
|
||||
_imageArrayRefs = Array.Empty<ArrayRef<ImageArray>>();
|
||||
_textureArrayRefs = [];
|
||||
_imageArrayRefs = [];
|
||||
|
||||
_textureArrayExtraRefs = Array.Empty<ArrayRef<TextureArray>>();
|
||||
_imageArrayExtraRefs = Array.Empty<ArrayRef<ImageArray>>();
|
||||
_textureArrayExtraRefs = [];
|
||||
_imageArrayExtraRefs = [];
|
||||
|
||||
_uniformBuffers = new DescriptorBufferInfo[Constants.MaxUniformBufferBindings];
|
||||
_storageBuffers = new DescriptorBufferInfo[Constants.MaxStorageBufferBindings];
|
||||
|
@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
_uniformSetPd = new int[Constants.MaxUniformBufferBindings];
|
||||
|
||||
var initialImageInfo = new DescriptorImageInfo
|
||||
DescriptorImageInfo initialImageInfo = new()
|
||||
{
|
||||
ImageLayout = ImageLayout.General,
|
||||
};
|
||||
|
@ -217,7 +217,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
if (isMainPipeline)
|
||||
{
|
||||
FeedbackLoopHazards = new();
|
||||
FeedbackLoopHazards = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,7 +235,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
// Check stage bindings
|
||||
|
||||
_uniformMirrored.Union(_uniformSet).SignalSet((int binding, int count) =>
|
||||
_uniformMirrored.Union(_uniformSet).SignalSet((binding, count) =>
|
||||
{
|
||||
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++)
|
||||
{
|
||||
|
@ -301,13 +301,13 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ref var arrayRef = ref _textureArrayRefs[segment.Binding];
|
||||
ref ArrayRef<TextureArray> arrayRef = ref _textureArrayRefs[segment.Binding];
|
||||
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
|
||||
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
|
||||
}
|
||||
|
@ -322,13 +322,13 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ref var arrayRef = ref _imageArrayRefs[segment.Binding];
|
||||
ref ArrayRef<ImageArray> arrayRef = ref _imageArrayRefs[segment.Binding];
|
||||
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
|
||||
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
|
||||
}
|
||||
|
@ -337,7 +337,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < _program.BindingSegments.Length; setIndex++)
|
||||
{
|
||||
var bindingSegments = _program.BindingSegments[setIndex];
|
||||
ResourceBindingSegment[] bindingSegments = _program.BindingSegments[setIndex];
|
||||
|
||||
if (bindingSegments.Length == 0)
|
||||
{
|
||||
|
@ -348,18 +348,18 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
if (segment.IsArray)
|
||||
{
|
||||
if (segment.Type == ResourceType.Texture ||
|
||||
segment.Type == ResourceType.Sampler ||
|
||||
segment.Type == ResourceType.TextureAndSampler ||
|
||||
segment.Type == ResourceType.BufferTexture)
|
||||
if (segment.Type is ResourceType.Texture or
|
||||
ResourceType.Sampler or
|
||||
ResourceType.TextureAndSampler or
|
||||
ResourceType.BufferTexture)
|
||||
{
|
||||
ref var arrayRef = ref _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
|
||||
ref ArrayRef<TextureArray> arrayRef = ref _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
|
||||
PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
|
||||
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();
|
||||
arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
|
||||
}
|
||||
|
@ -424,8 +424,8 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
{
|
||||
for (int i = 0; i < buffers.Length; i++)
|
||||
{
|
||||
var assignment = buffers[i];
|
||||
var buffer = assignment.Range;
|
||||
BufferAssignment assignment = buffers[i];
|
||||
BufferRange buffer = assignment.Range;
|
||||
int index = assignment.Binding;
|
||||
|
||||
Auto<DisposableBuffer> vkBuffer = buffer.Handle == BufferHandle.Null
|
||||
|
@ -440,7 +440,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
Range = (ulong)buffer.Size,
|
||||
};
|
||||
|
||||
var newRef = new BufferRef(vkBuffer, ref buffer);
|
||||
BufferRef newRef = new(vkBuffer, ref buffer);
|
||||
|
||||
ref DescriptorBufferInfo currentInfo = ref _storageBuffers[index];
|
||||
|
||||
|
@ -460,7 +460,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
{
|
||||
for (int i = 0; i < buffers.Length; i++)
|
||||
{
|
||||
var vkBuffer = buffers[i];
|
||||
Auto<DisposableBuffer> vkBuffer = buffers[i];
|
||||
int index = first + i;
|
||||
|
||||
ref BufferRef currentBufferRef = ref _storageBufferRefs[index];
|
||||
|
@ -633,8 +633,8 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
{
|
||||
for (int i = 0; i < buffers.Length; i++)
|
||||
{
|
||||
var assignment = buffers[i];
|
||||
var buffer = assignment.Range;
|
||||
BufferAssignment assignment = buffers[i];
|
||||
BufferRange buffer = assignment.Range;
|
||||
int index = assignment.Binding;
|
||||
|
||||
Auto<DisposableBuffer> vkBuffer = buffer.Handle == BufferHandle.Null
|
||||
|
@ -678,7 +678,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
return;
|
||||
}
|
||||
|
||||
var program = _program;
|
||||
ShaderCollection program = _program;
|
||||
|
||||
if (_dirty.HasFlag(DirtyFlags.Uniform))
|
||||
{
|
||||
|
@ -699,13 +699,19 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
if (_dirty.HasFlag(DirtyFlags.Texture))
|
||||
{
|
||||
if (program.UpdateTexturesWithoutTemplate)
|
||||
if (false)
|
||||
{
|
||||
UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dummyBuffer = _dummyBuffer?.GetBuffer();
|
||||
Auto<DisposableBuffer> dummyBuffer = _dummyBuffer?.GetBuffer();
|
||||
|
||||
if (_updateDescriptorCacheCbIndex)
|
||||
{
|
||||
|
@ -776,7 +781,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
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)
|
||||
{
|
||||
|
@ -811,12 +816,9 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
}
|
||||
}
|
||||
|
||||
// Split buffer updates into individual slices for MoltenVK compatibility
|
||||
ReadOnlySpan<DescriptorBufferInfo> uniformBuffers = _uniformBuffers;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
tu.Push(uniformBuffers.Slice(binding + i, 1));
|
||||
}
|
||||
|
||||
tu.Push(uniformBuffers.Slice(binding, count));
|
||||
}
|
||||
else if (setIndex == PipelineBase.StorageSetIndex)
|
||||
{
|
||||
|
@ -828,7 +830,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
if (_storageSet.Set(index))
|
||||
{
|
||||
ref var info = ref _storageBuffers[index];
|
||||
ref DescriptorBufferInfo info = ref _storageBuffers[index];
|
||||
|
||||
bool mirrored = UpdateBuffer(cbs,
|
||||
ref info,
|
||||
|
@ -840,12 +842,9 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
}
|
||||
}
|
||||
|
||||
// Split buffer updates into individual slices for MoltenVK compatibility
|
||||
ReadOnlySpan<DescriptorBufferInfo> storageBuffers = _storageBuffers;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
tu.Push(storageBuffers.Slice(binding + i, 1));
|
||||
}
|
||||
|
||||
tu.Push(storageBuffers.Slice(binding, count));
|
||||
}
|
||||
else if (setIndex == PipelineBase.TextureSetIndex)
|
||||
{
|
||||
|
@ -857,8 +856,8 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ref var texture = ref textures[i];
|
||||
ref var refs = ref _textureRefs[binding + i];
|
||||
ref DescriptorImageInfo texture = ref textures[i];
|
||||
ref TextureRef refs = ref _textureRefs[binding + i];
|
||||
|
||||
texture.ImageView = refs.ImageView?.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.Slice(i, 1));
|
||||
}
|
||||
tu.Push<DescriptorImageInfo>(textures[..count]);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -888,10 +884,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
bufferTextures[i] = _bufferTextureRefs[binding + i]?.GetBufferView(cbs, false) ?? default;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
tu.Push<BufferView>(bufferTextures.Slice(i, 1));
|
||||
}
|
||||
tu.Push<BufferView>(bufferTextures[..count]);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -919,10 +912,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
images[i].ImageView = _imageRefs[binding + i].ImageView?.Get(cbs).Value ?? default;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
tu.Push<DescriptorImageInfo>(images.Slice(i, 1));
|
||||
}
|
||||
tu.Push<DescriptorImageInfo>(images[..count]);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -933,10 +923,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
bufferImages[i] = _bufferImageRefs[binding + i]?.GetBufferView(cbs, true) ?? default;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
tu.Push<BufferView>(bufferImages.Slice(i, 1));
|
||||
}
|
||||
tu.Push<BufferView>(bufferImages[..count]);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -953,31 +940,29 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
}
|
||||
}
|
||||
|
||||
var sets = dsc.GetSets();
|
||||
DescriptorSet[] sets = dsc.GetSets();
|
||||
_templateUpdater.Commit(_gd, _device, sets[0]);
|
||||
|
||||
_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)
|
||||
{
|
||||
int setIndex = PipelineBase.TextureSetIndex;
|
||||
var bindingSegments = program.BindingSegments[setIndex];
|
||||
ResourceBindingSegment[] bindingSegments = program.BindingSegments[setIndex];
|
||||
|
||||
if (bindingSegments.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dummyImageInfo = new DescriptorImageInfo
|
||||
if (_updateDescriptorCacheCbIndex)
|
||||
{
|
||||
ImageView = _dummyTexture.GetImageView().Get(cbs).Value,
|
||||
Sampler = _dummySampler.GetSampler().Get(cbs).Value,
|
||||
ImageLayout = ImageLayout.General
|
||||
};
|
||||
_updateDescriptorCacheCbIndex = false;
|
||||
program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
|
||||
}
|
||||
|
||||
var dsc = program.GetNewDescriptorSetCollection(setIndex, out _).Get(cbs);
|
||||
DescriptorSetCollection dsc = program.GetNewDescriptorSetCollection(setIndex, out _).Get(cbs);
|
||||
|
||||
foreach (ResourceBindingSegment segment in bindingSegments)
|
||||
{
|
||||
|
@ -988,78 +973,56 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
{
|
||||
if (segment.Type != ResourceType.BufferTexture)
|
||||
{
|
||||
Span<DescriptorImageInfo> textures = _textures;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int index = binding + i;
|
||||
ref var textureRef = ref _textureRefs[index];
|
||||
ref DescriptorImageInfo texture = ref textures[i];
|
||||
ref TextureRef refs = ref _textureRefs[binding + i];
|
||||
|
||||
var imageView = textureRef.ImageView?.Get(cbs).Value ?? dummyImageInfo.ImageView;
|
||||
var sampler = textureRef.Sampler?.Get(cbs).Value ?? dummyImageInfo.Sampler;
|
||||
texture.ImageView = refs.ImageView?.Get(cbs).Value ?? default;
|
||||
texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
|
||||
|
||||
var imageInfo = new DescriptorImageInfo
|
||||
if (texture.ImageView.Handle == 0)
|
||||
{
|
||||
ImageView = imageView.Handle != 0 ? imageView : dummyImageInfo.ImageView,
|
||||
Sampler = sampler.Handle != 0 ? sampler : dummyImageInfo.Sampler,
|
||||
ImageLayout = ImageLayout.General
|
||||
};
|
||||
texture.ImageView = _dummyTexture.GetImageView().Get(cbs).Value;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
Span<BufferView> bufferTextures = _bufferTextures;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int index = binding + i;
|
||||
var bufferView = _bufferTextureRefs[index]?.GetBufferView(cbs, false) ?? default;
|
||||
dsc.UpdateBufferImages(0, index, new[] { bufferView }, DescriptorType.UniformTexelBuffer);
|
||||
bufferTextures[i] = _bufferTextureRefs[binding + i]?.GetBufferView(cbs, false) ?? default;
|
||||
}
|
||||
|
||||
dsc.UpdateBufferImages(0, binding, bufferTextures[..count], DescriptorType.UniformTexelBuffer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var arrayRef = _textureArrayRefs[binding];
|
||||
|
||||
if (segment.Type != ResourceType.BufferTexture)
|
||||
{
|
||||
var imageInfos = arrayRef.Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler);
|
||||
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);
|
||||
}
|
||||
}
|
||||
dsc.UpdateImages(0, binding, _textureArrayRefs[binding].Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler), DescriptorType.CombinedImageSampler);
|
||||
}
|
||||
else
|
||||
{
|
||||
var bufferViews = arrayRef.Array.GetBufferViews(cbs);
|
||||
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);
|
||||
}
|
||||
}
|
||||
dsc.UpdateBufferImages(0, binding, _textureArrayRefs[binding].Array.GetBufferViews(cbs), 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);
|
||||
}
|
||||
|
||||
|
@ -1067,8 +1030,8 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs)
|
||||
{
|
||||
int sequence = _pdSequence;
|
||||
var bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex];
|
||||
var dummyBuffer = _dummyBuffer?.GetBuffer();
|
||||
ResourceBindingSegment[] bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex];
|
||||
Auto<DisposableBuffer> dummyBuffer = _dummyBuffer?.GetBuffer();
|
||||
|
||||
long updatedBindings = 0;
|
||||
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)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
var dummyBuffer = _dummyBuffer?.GetBuffer().Get(cbs).Value ?? default;
|
||||
Buffer dummyBuffer = _dummyBuffer?.GetBuffer().Get(cbs).Value ?? default;
|
||||
|
||||
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++)
|
||||
{
|
||||
var bindingSegments = program.BindingSegments[setIndex];
|
||||
ResourceBindingSegment[] bindingSegments = program.BindingSegments[setIndex];
|
||||
|
||||
if (bindingSegments.Length == 0)
|
||||
{
|
||||
|
@ -1145,10 +1108,10 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
{
|
||||
DescriptorSet[] sets = null;
|
||||
|
||||
if (segment.Type == ResourceType.Texture ||
|
||||
segment.Type == ResourceType.Sampler ||
|
||||
segment.Type == ResourceType.TextureAndSampler ||
|
||||
segment.Type == ResourceType.BufferTexture)
|
||||
if (segment.Type is ResourceType.Texture or
|
||||
ResourceType.Sampler or
|
||||
ResourceType.TextureAndSampler or
|
||||
ResourceType.BufferTexture)
|
||||
{
|
||||
sets = _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets(
|
||||
_device,
|
||||
|
@ -1159,7 +1122,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
_dummyTexture,
|
||||
_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(
|
||||
_device,
|
||||
|
@ -1230,4 +1193,4 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
Dispose(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -186,20 +186,18 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
return sets;
|
||||
}
|
||||
|
||||
DescriptorSetTemplate template = program.Templates[setIndex];
|
||||
|
||||
DescriptorSetTemplateWriter tu = templateUpdater.Begin(template);
|
||||
var dsc = program.GetNewDescriptorSetCollection(setIndex, out var isNew).Get(cbs);
|
||||
|
||||
if (!_isBuffer)
|
||||
{
|
||||
tu.Push(GetImageInfos(_gd, cbs, dummyTexture));
|
||||
dsc.UpdateImages(0, 0, GetImageInfos(_gd, cbs, dummyTexture), DescriptorType.StorageImage);
|
||||
}
|
||||
else
|
||||
{
|
||||
tu.Push(GetBufferViews(cbs));
|
||||
dsc.UpdateBufferImages(0, 0, GetBufferViews(cbs), DescriptorType.StorageTexelBuffer);
|
||||
}
|
||||
|
||||
templateUpdater.Commit(_gd, device, sets[0]);
|
||||
sets = dsc.GetSets();
|
||||
|
||||
return sets;
|
||||
}
|
||||
|
|
|
@ -785,7 +785,7 @@ namespace Ryujinx.Graphics.Vulkan
|
|||
shaderSubgroupSize: (int)Capabilities.SubgroupSize,
|
||||
storageBufferOffsetAlignment: (int)limits.MinStorageBufferOffsetAlignment,
|
||||
textureBufferOffsetAlignment: (int)limits.MinTexelBufferOffsetAlignment,
|
||||
gatherBiasPrecision: (int)Capabilities.SubTexelPrecisionBits, //IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0,
|
||||
gatherBiasPrecision: IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0,
|
||||
maximumGpuMemory: GetTotalGPUMemory());
|
||||
}
|
||||
|
||||
|
|
|
@ -21,25 +21,25 @@ namespace Ryujinx.HLE
|
|||
/// The virtual file system used by the FS service.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly VirtualFileSystem VirtualFileSystem;
|
||||
public readonly VirtualFileSystem VirtualFileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// The manager for handling a LibHac Horizon instance.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly LibHacHorizonManager LibHacHorizonManager;
|
||||
public readonly LibHacHorizonManager LibHacHorizonManager;
|
||||
|
||||
/// <summary>
|
||||
/// The account manager used by the account service.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly AccountManager AccountManager;
|
||||
public readonly AccountManager AccountManager;
|
||||
|
||||
/// <summary>
|
||||
/// The content manager used by the NCM service.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly ContentManager ContentManager;
|
||||
public readonly ContentManager ContentManager;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly IRenderer GpuRenderer;
|
||||
public readonly IRenderer GpuRenderer;
|
||||
|
||||
/// <summary>
|
||||
/// The audio device driver to use for all audio operations.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly IHardwareDeviceDriver AudioDeviceDriver;
|
||||
public readonly IHardwareDeviceDriver AudioDeviceDriver;
|
||||
|
||||
/// <summary>
|
||||
/// The handler for various UI related operations needed outside of HLE.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly IHostUIHandler HostUIHandler;
|
||||
public readonly IHostUIHandler HostUIHandler;
|
||||
|
||||
/// <summary>
|
||||
/// Control the memory configuration used by the emulation context.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly MemoryConfiguration MemoryConfiguration;
|
||||
public readonly MemoryConfiguration MemoryConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// The system language to use in the settings service.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly SystemLanguage SystemLanguage;
|
||||
public readonly SystemLanguage SystemLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// The system region to use in the settings service.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly RegionCode Region;
|
||||
public readonly RegionCode Region;
|
||||
|
||||
/// <summary>
|
||||
/// Control the initial state of the vertical sync in the SurfaceFlinger service.
|
||||
/// </summary>
|
||||
internal readonly bool EnableVsync;
|
||||
public readonly bool EnableVsync;
|
||||
|
||||
/// <summary>
|
||||
/// Control the initial state of the docked mode.
|
||||
/// </summary>
|
||||
internal readonly bool EnableDockedMode;
|
||||
public readonly bool EnableDockedMode;
|
||||
|
||||
/// <summary>
|
||||
/// Control if the Profiled Translation Cache (PTC) should be used.
|
||||
/// </summary>
|
||||
internal readonly bool EnablePtc;
|
||||
public readonly bool EnablePtc;
|
||||
|
||||
/// <summary>
|
||||
/// Control if the guest application should be told that there is a Internet connection available.
|
||||
/// </summary>
|
||||
public bool EnableInternetAccess { internal get; set; }
|
||||
public bool EnableInternetAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Control LibHac's integrity check level.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly IntegrityCheckLevel FsIntegrityCheckLevel;
|
||||
public readonly IntegrityCheckLevel FsIntegrityCheckLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Control LibHac's global access logging level. Value must be between 0 and 3.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly int FsGlobalAccessLogMode;
|
||||
public readonly int FsGlobalAccessLogMode;
|
||||
|
||||
/// <summary>
|
||||
/// The system time offset to apply to the time service steady and local clocks.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly long SystemTimeOffset;
|
||||
public readonly long SystemTimeOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The system timezone used by the time service.
|
||||
/// </summary>
|
||||
/// <remarks>This cannot be changed after <see cref="Switch"/> instantiation.</remarks>
|
||||
internal readonly string TimeZone;
|
||||
public readonly string TimeZone;
|
||||
|
||||
/// <summary>
|
||||
/// Type of the memory manager used on CPU emulation.
|
||||
/// </summary>
|
||||
public MemoryManagerMode MemoryManagerMode { internal get; set; }
|
||||
public MemoryManagerMode MemoryManagerMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// TODO: Update this again.
|
||||
public bool IgnoreMissingServices { internal get; set; }
|
||||
public bool IgnoreMissingServices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Aspect Ratio applied to the renderer window by the SurfaceFlinger service.
|
||||
|
@ -152,22 +152,22 @@ namespace Ryujinx.HLE
|
|||
/// <summary>
|
||||
/// Use Hypervisor over JIT if available.
|
||||
/// </summary>
|
||||
internal readonly bool UseHypervisor;
|
||||
public readonly bool UseHypervisor;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplayer LAN Interface ID (device GUID)
|
||||
/// </summary>
|
||||
public string MultiplayerLanInterfaceId { internal get; set; }
|
||||
public string MultiplayerLanInterfaceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Multiplayer Mode
|
||||
/// </summary>
|
||||
public MultiplayerMode MultiplayerMode { internal get; set; }
|
||||
public MultiplayerMode MultiplayerMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An action called when HLE force a refresh of output after docked mode changed.
|
||||
/// </summary>
|
||||
public Action RefreshInputConfig { internal get; set; }
|
||||
public Action RefreshInputConfig { get; set; }
|
||||
|
||||
public HLEConfiguration(VirtualFileSystem virtualFileSystem,
|
||||
LibHacHorizonManager libHacHorizonManager,
|
||||
|
|
|
@ -396,7 +396,7 @@ namespace Ryujinx.Headless.SDL2
|
|||
[UnmanagedCallersOnly(EntryPoint = "pause_emulation")]
|
||||
public static void PauseEmulation(bool shouldPause)
|
||||
{
|
||||
if (_window != null)
|
||||
if (_window != null && _window.Device != null)
|
||||
{
|
||||
if (!shouldPause)
|
||||
{
|
||||
|
@ -1721,5 +1721,137 @@ namespace Ryujinx.Headless.SDL2
|
|||
span.Clear();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ namespace Ryujinx.Headless.SDL2
|
|||
|
||||
private string _gpuDriverName;
|
||||
|
||||
private readonly AspectRatio _aspectRatio;
|
||||
public AspectRatio _aspectRatio;
|
||||
private readonly bool _enableMouse;
|
||||
|
||||
public WindowBase(
|
||||
|
@ -162,7 +162,7 @@ namespace Ryujinx.Headless.SDL2
|
|||
|
||||
private void InitializeWindow()
|
||||
{
|
||||
if (this is Ryujinx.Headless.SDL2.Vulkan.MoltenVKWindow) {
|
||||
if (this is Vulkan.MoltenVKWindow) {
|
||||
string message = $"Not using SDL Windows, Skipping...";
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, message);
|
||||
|
|