0
0
mirror of https://git.743378673.xyz/MeloNX/MeloNX.git synced 2025-04-24 08:25:14 +00:00

Add Fixed Handheld mode, Location to keep game running in the background, New Airplay Menu amd more

This commit is contained in:
Stossy11 2025-04-10 22:30:56 +10:00
parent 15171a703a
commit e382a35387
13 changed files with 381 additions and 183 deletions

View File

@ -27,7 +27,7 @@
{
"version": "1.7.0",
"buildVersion": "1",
"date": "2025-04-8",
"date": "2025-04-08",
"localizedDescription": "First AltStore release!",
"downloadURL": "https://git.743378673.xyz/MeloNX/MeloNX/releases/download/1.7.0/MeloNX.ipa",
"size": 79821,
@ -40,7 +40,7 @@
"com.apple.developer.kernel.increased-memory-limit"
],
"privacy": {
"NSPhotoLibraryAddUsageDescription": "MeloNX needs access to your Photo Library in order to save images."
"NSPhotoLibraryAddUsageDescription": "MeloNX needs access to your Photo Library in order to save screenshots."
}
}
}

View File

@ -3,7 +3,6 @@ using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Memory;
using ARMeilleure.Native;
using Ryujinx.Memory;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -16,73 +15,52 @@ namespace ARMeilleure.Translation.Cache
static partial class JitCache
{
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
private static readonly int _pageMask = _pageSize - 4;
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 128 * 1024 * 1024;
private const int CacheSizeIOS = 128 * 1024 * 1024;
private const int CacheSize = 2047 * 1024 * 1024;
private const int CacheSizeIOS = 1024 * 1024 * 1024;
private static ReservedRegion _jitRegion;
private static JitCacheInvalidation _jitCacheInvalidator;
private static List<CacheMemoryAllocator> _cacheAllocators = [];
private static CacheMemoryAllocator _cacheAllocator;
private static readonly List<CacheEntry> _cacheEntries = new();
private static readonly object _lock = new();
private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = new();
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial IntPtr FlushInstructionCache(IntPtr hProcess, IntPtr lpAddress, UIntPtr dwSize);
public static void Initialize(IJitMemoryAllocator allocator)
{
if (_initialized)
{
return;
}
lock (_lock)
{
if (_initialized)
{
if (OperatingSystem.IsWindows())
{
// JitUnwindWindows.RemoveFunctionTableHandler(
// _jitRegions[0].Pointer);
return;
}
for (int i = 0; i < _jitRegions.Count; i++)
{
_jitRegions[i].Dispose();
}
_jitRegions.Clear();
_cacheAllocators.Clear();
}
else
{
_initialized = true;
}
_activeRegionIndex = 0;
var firstRegion = new ReservedRegion(allocator, CacheSize);
_jitRegions.Add(firstRegion);
CacheMemoryAllocator firstCacheAllocator = new(CacheSize);
_cacheAllocators.Add(firstCacheAllocator);
_jitRegion = new ReservedRegion(allocator, (ulong)(OperatingSystem.IsIOS() ? CacheSizeIOS : CacheSize));
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
{
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
}
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.InstallFunctionTableHandler(
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
);
JitUnwindWindows.InstallFunctionTableHandler(_jitRegion.Pointer, CacheSize, _jitRegion.Pointer + Allocate(_pageSize));
}
_initialized = true;
@ -95,9 +73,7 @@ namespace ARMeilleure.Translation.Cache
{
while (_deferredRxProtect.TryDequeue(out var result))
{
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
ReprotectAsExecutable(targetRegion, result.funcOffset, result.length);
ReprotectAsExecutable(result.funcOffset, result.length);
}
}
@ -111,8 +87,7 @@ namespace ARMeilleure.Translation.Cache
int funcOffset = Allocate(code.Length, deferProtect);
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
IntPtr funcPtr = targetRegion.Pointer + funcOffset;
IntPtr funcPtr = _jitRegion.Pointer + funcOffset;
if (OperatingSystem.IsIOS())
{
@ -123,7 +98,8 @@ namespace ARMeilleure.Translation.Cache
}
else
{
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
ReprotectAsExecutable(funcOffset, code.Length);
JitSupportDarwinAot.Invalidate(funcPtr, (ulong)code.Length);
}
}
@ -139,9 +115,9 @@ namespace ARMeilleure.Translation.Cache
}
else
{
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
ReprotectAsWritable(funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
ReprotectAsExecutable(funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@ -163,50 +139,41 @@ namespace ARMeilleure.Translation.Cache
{
if (OperatingSystem.IsIOS())
{
// return;
return;
}
lock (_lock)
{
foreach (var region in _jitRegions)
{
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
{
continue;
}
Debug.Assert(_initialized);
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
int funcOffset = (int)(pointer.ToInt64() - _jitRegion.Pointer.ToInt64());
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocators[_activeRegionIndex].Free(funcOffset, AlignCodeSize(entry.Size));
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
return;
}
}
}
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
private static void ReprotectAsWritable(int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
_jitRegion.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
private static void ReprotectAsExecutable(int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
_jitRegion.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static int Allocate(int codeSize, bool deferProtect = false)
@ -220,35 +187,20 @@ namespace ARMeilleure.Translation.Cache
alignment = 0x4000;
}
int allocOffset = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment);
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
if (allocOffset >= 0)
Console.WriteLine($"{allocOffset:x8}: {codeSize:x8} {alignment:x8}");
if (allocOffset < 0)
{
_jitRegions[_activeRegionIndex].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
throw new OutOfMemoryException("JIT Cache exhausted.");
}
_jitRegion.ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
return allocOffset;
}
int exhaustedRegion = _activeRegionIndex;
var newRegion = new ReservedRegion(_jitRegions[0].Allocator, CacheSize);
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
int newRegionNumber = _activeRegionIndex;
Logger.Info?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {_activeRegionIndex} ({((long)(_activeRegionIndex + 1) * CacheSize)} Total Allocation).");
_cacheAllocators.Add(new CacheMemoryAllocator(CacheSize));
int allocOffsetNew = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment);
if (allocOffsetNew < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
}
private static int AlignCodeSize(int codeSize, bool deferProtect = false)
{
int alignment = CodeAlignment;

View File

@ -724,6 +724,10 @@
"$(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;
@ -896,6 +900,16 @@
"$(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;
@ -919,7 +933,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = 95J8WZ4TN8;
DEVELOPMENT_TEAM = 4D52P7Z7YN;
ENABLE_PREVIEWS = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = (
@ -1000,6 +1014,10 @@
"$(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;
@ -1172,9 +1190,19 @@
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
);
MARKETING_VERSION = "$(VERSION)";
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
PRODUCT_BUNDLE_IDENTIFIER = "com.stossy11.MeloNX-personal";
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;

View File

@ -50,7 +50,7 @@ char* installed_firmware_version();
void set_native_window(void *layerPtr);
void stop_emulation();
void stop_emulation(bool shouldPause);
void initialize();

View File

@ -371,7 +371,6 @@ class Ryujinx : ObservableObject {
self.emulationUIView = nil
self.metalLayer = nil
stop_emulation()
thread.cancel()
}

View File

@ -10,6 +10,7 @@ import GameController
import Darwin
import UIKit
import MetalKit
import CoreLocation
struct MoltenVKSettings: Codable, Hashable {
let string: String
@ -159,16 +160,7 @@ struct ContentView: View {
initControllerObservers()
Air.play(AnyView(
VStack {
Image(systemName: "gamecontroller")
.font(.system(size: 300))
.foregroundColor(.gray)
.padding(.bottom, 10)
Text("Select Game")
.font(.system(size: 150))
.bold()
}
ControllerListView(game: $game)
))
checkJitStatus()
@ -310,6 +302,8 @@ struct ContentView: View {
}
private func setupEmulation() {
refreshControllersList()
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
DispatchQueue.main.async {
@ -327,6 +321,25 @@ struct ContentView: View {
controllersList.removeAll(where: { $0.id == "0" || (!$0.name.starts(with: "GC - ") && $0 != onscreencontroller) })
controllersList.mutableForEach { $0.name = $0.name.replacingOccurrences(of: "GC - ", with: "") }
if !currentControllers.isEmpty, !(currentControllers.count == 1) {
var currentController: [Controller] = []
if currentController.count == 1 {
currentController.append(controllersList[0])
} else if (controllersList.count - 1) >= 1 {
for controller in controllersList {
if controller.id != onscreencontroller.id && !currentControllers.contains(where: { $0.id == controller.id }) {
currentController.append(controller)
}
}
}
if currentController == currentControllers {
currentControllers = []
currentControllers = currentController
}
} else {
currentControllers = []
if controllersList.count == 1 {
@ -339,6 +352,7 @@ struct ContentView: View {
}
}
}
}
private func start(displayid: UInt32) {
guard let game else { return }
@ -397,6 +411,7 @@ struct ContentView: View {
private func handleDeepLink(_ url: URL) {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.host == "game" {
DispatchQueue.main.async {
if let text = components.queryItems?.first(where: { $0.name == "id" })?.value {
game = ryujinx.games.first(where: { $0.titleId == text })
} else if let text = components.queryItems?.first(where: { $0.name == "name" })?.value {
@ -405,6 +420,7 @@ struct ContentView: View {
}
}
}
}
extension Array {
@inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
@ -413,3 +429,136 @@ extension Array {
}
}
}
class LocationManager: NSObject, CLLocationManagerDelegate {
private var locationManager: CLLocationManager
static let sharedInstance = LocationManager()
private override init() {
locationManager = CLLocationManager()
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.pausesLocationUpdatesAutomatically = false
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// print("wow")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location manager failed with: \(error)")
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .denied {
print("Location services are disabled in settings.")
} else {
startUpdatingLocation()
}
}
func stop() {
if UserDefaults.standard.bool(forKey: "location-enabled") {
locationManager.stopUpdatingLocation()
}
}
func startUpdatingLocation() {
if UserDefaults.standard.bool(forKey: "location-enabled") {
locationManager.requestAlwaysAuthorization()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.startUpdatingLocation()
}
}
}
struct ControllerListView: View {
@State private var selectedIndex = 0
@Binding var game: Game?
@ObservedObject private var ryujinx = Ryujinx.shared
var body: some View {
List(ryujinx.games.indices, id: \.self) { index in
let game = ryujinx.games[index]
HStack(spacing: 16) {
// Game Icon
Group {
if let icon = game.icon {
Image(uiImage: icon)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
ZStack {
RoundedRectangle(cornerRadius: 10)
Image(systemName: "gamecontroller.fill")
.font(.system(size: 24))
.foregroundColor(.gray)
}
}
}
.frame(width: 55, height: 55)
.cornerRadius(10)
// Game Info
VStack(alignment: .leading, spacing: 4) {
Text(game.titleName)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.primary)
HStack(spacing: 4) {
Text(game.developer)
if !game.version.isEmpty && game.version != "0" {
Text("")
Text("v\(game.version)")
}
}
.font(.system(size: 14))
.foregroundColor(.secondary)
}
Spacer()
}
.background(selectedIndex == index ? Color.blue.opacity(0.3) : .clear)
}
.onAppear(perform: setupControllerObservers)
}
private func setupControllerObservers() {
let dpadHandler: GCControllerDirectionPadValueChangedHandler = { _, _, yValue in
if yValue == 1.0 {
selectedIndex = max(0, selectedIndex - 1)
} else if yValue == -1.0 {
selectedIndex = min(ryujinx.games.count - 1, selectedIndex + 1)
}
}
for controller in GCController.controllers() {
print("Controller connected: \(controller.vendorName ?? "Unknown")")
controller.playerIndex = .index1
controller.microGamepad?.dpad.valueChangedHandler = dpadHandler
controller.extendedGamepad?.dpad.valueChangedHandler = dpadHandler
controller.extendedGamepad?.buttonA.pressedChangedHandler = { _, _, pressed in
if pressed {
print("A button pressed")
game = ryujinx.games[selectedIndex]
}
}
}
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main
) { _ in
setupControllerObservers()
}
}
}

View File

@ -19,6 +19,9 @@ struct EmulationView: View {
@Binding var startgame: Game?
@Environment(\.scenePhase) var scenePhase
@State private var isInBackground = false
@AppStorage("location-enabled") var locationenabled: Bool = false
var body: some View {
ZStack {
if isAirplaying {
@ -26,7 +29,7 @@ struct EmulationView: View {
.ignoresSafeArea()
.edgesIgnoringSafeArea(.all)
.onAppear {
Air.play(AnyView(MetalView().ignoresSafeArea()))
Air.play(AnyView(MetalView().ignoresSafeArea().edgesIgnoringSafeArea(.all)))
}
} else {
MetalView() // The Emulation View
@ -88,6 +91,7 @@ struct EmulationView: View {
}
}
.onAppear {
LocationManager.sharedInstance.startUpdatingLocation()
Air.shared.connectionCallbacks.append { cool in
DispatchQueue.main.async {
isAirplaying = cool
@ -95,5 +99,18 @@ struct EmulationView: View {
}
}
}
.onChange(of: scenePhase) { newPhase in
// Detect when the app enters the background
if newPhase == .background {
stop_emulation(true)
isInBackground = true
} else if newPhase == .active {
stop_emulation(false)
isInBackground = false
} else if newPhase == .inactive {
stop_emulation(true)
isInBackground = true
}
}
}
}

View File

@ -429,61 +429,16 @@ struct SettingsView: View {
Text("Controller Selection")
.font(.headline)
.foregroundColor(.primary)
if currentControllers.isEmpty {
emptyControllersView
} else {
controllerListView
}
if hasAvailableControllers {
Divider()
if !currentControllers.isEmpty {
ForEach(currentControllers) { controller in
if currentControllers.firstIndex(of: controller) == currentControllers.count - 1 && currentControllers.count != 1 {
Divider()
}
HStack {
Image(systemName: "gamecontroller.fill")
.foregroundColor(.blue)
if let index = currentControllers.firstIndex(where: { $0.id == controller.id }) {
Text("Player \(index + 1): \(controller.name)")
.lineLimit(1)
}
Spacer()
Button {
toggleController(controller)
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
if currentControllers[0] != controller && currentControllers.firstIndex(of: controller) == currentControllers.count - 1 {
Divider()
}
}
.onMove { from, to in
currentControllers.move(fromOffsets: from, toOffset: to)
}
.environment(\.editMode, .constant(.active))
}
if !controllersList.filter({ !currentControllers.contains($0) }).isEmpty {
Divider()
Menu {
ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in
Button {
currentControllers.append(controller)
} label: {
Text(controller.name)
}
}
} label: {
Label("Add Controller", systemImage: "plus.circle.fill")
.foregroundColor(.blue)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 6)
}
addControllerButton
}
}
}
@ -557,6 +512,78 @@ struct SettingsView: View {
}
}
// MARK: - Controller Selection Components
private var hasAvailableControllers: Bool {
!controllersList.filter { !currentControllers.contains($0) }.isEmpty
}
private var emptyControllersView: some View {
HStack {
Text("No controllers selected (Keyboard will be used)")
.foregroundColor(.secondary)
.italic()
Spacer()
}
.padding(.vertical, 8)
}
private var controllerListView: some View {
VStack(spacing: 0) {
Divider()
ForEach(currentControllers.indices, id: \.self) { index in
let controller = currentControllers[index]
VStack(spacing: 0) {
HStack {
Image(systemName: "gamecontroller.fill")
.foregroundColor(.blue)
Text("Player \(index + 1): \(controller.name)")
.lineLimit(1)
Spacer()
Button {
toggleController(controller)
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
.padding(.vertical, 8)
if index < currentControllers.count - 1 {
Divider()
}
}
}
.onMove { from, to in
currentControllers.move(fromOffsets: from, toOffset: to)
}
.environment(\.editMode, .constant(.active))
}
}
private var addControllerButton: some View {
Menu {
ForEach(controllersList.filter { !currentControllers.contains($0) }) { controller in
Button {
currentControllers.append(controller)
} label: {
Text(controller.name)
}
}
} label: {
Label("Add Controller", systemImage: "plus.circle.fill")
.foregroundColor(.blue)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 6)
}
}
// MARK: - System Settings
private var systemSettings: some View {

View File

@ -9,6 +9,8 @@ import SwiftUI
import UIKit
import CryptoKit
import UniformTypeIdentifiers
import AVFoundation
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
@ -29,6 +31,8 @@ struct MeloNXApp: App {
@State var finished = false
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
@AppStorage("location-enabled") var locationenabled: Bool = false
var body: some Scene {
WindowGroup {
if finishedStorage {

View File

@ -38,8 +38,9 @@
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>location</string>
<string>processing</string>
<string>audio</string>
</array>
<key>UIFileSharingEnabled</key>
<true/>

View File

@ -403,10 +403,22 @@ namespace Ryujinx.Headless.SDL2
}
[UnmanagedCallersOnly(EntryPoint = "stop_emulation")]
public static void StopEmulation()
public static void StopEmulation(bool shouldPause)
{
if (_window != null)
{
if (!shouldPause)
{
_window.Device.SetVolume(1);
_window._isPaused = false;
_window._pauseEvent.Set();
}
else
{
_window.Device.SetVolume(0);
_window._isPaused = true;
_window._pauseEvent.Reset();
}
}
}
@ -885,21 +897,11 @@ namespace Ryujinx.Headless.SDL2
private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index, Options option)
{
if (inputId == null)
{
if (index == PlayerIndex.Player1)
{
Logger.Info?.Print(LogClass.Application, $"{index} not configured, defaulting to default keyboard.");
// Default to keyboard
inputId = "0";
}
else
{
Logger.Info?.Print(LogClass.Application, $"{index} not configured");
return null;
}
}
IGamepad gamepad;
@ -989,12 +991,26 @@ namespace Ryujinx.Headless.SDL2
{
bool isNintendoStyle = true; // gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons");
ControllerType currentController;
if (index == PlayerIndex.Handheld)
{
currentController = ControllerType.Handheld;
}
else if (gamepadName.Contains("Joycons") || gamepadName.Contains("Backbone"))
{
currentController = ControllerType.JoyconPair;
}
else
{
currentController = ControllerType.ProController;
}
config = new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL2,
Id = null,
ControllerType = ControllerType.JoyconPair,
ControllerType = currentController,
DeadzoneLeft = 0.1f,
DeadzoneRight = 0.1f,
RangeLeft = 1.0f,

View File

@ -44,6 +44,9 @@ namespace Ryujinx.Headless.SDL2
_mainThreadActions.Enqueue(action);
}
public bool _isPaused;
public ManualResetEvent _pauseEvent;
public NpadManager NpadManager;
public TouchScreenManager TouchScreenManager;
public Switch Device;
@ -104,6 +107,7 @@ namespace Ryujinx.Headless.SDL2
_gpuCancellationTokenSource = new CancellationTokenSource();
_exitEvent = new ManualResetEvent(false);
_gpuDoneEvent = new ManualResetEvent(false);
_pauseEvent = new ManualResetEvent(true);
_aspectRatio = aspectRatio;
_enableMouse = enableMouse;
HostUITheme = new HeadlessHostUiTheme();
@ -298,6 +302,8 @@ namespace Ryujinx.Headless.SDL2
return;
}
_pauseEvent.WaitOne();
_ticks += _chrono.ElapsedTicks;
_chrono.Restart();
@ -378,7 +384,6 @@ namespace Ryujinx.Headless.SDL2
{
while (_isActive)
{
UpdateFrame();
SDL_PumpEvents();