forked from MeloNX/MeloNX
Rewrite Display Code and more
This commit is contained in:
parent
e372f6eb35
commit
8ca88def54
@ -25,7 +25,7 @@ namespace ARMeilleure.Translation.Cache
|
||||
private static ReservedRegion _jitRegion;
|
||||
private static JitCacheInvalidation _jitCacheInvalidator;
|
||||
|
||||
private static CacheMemoryAllocator _cacheAllocator;
|
||||
private static List<CacheMemoryAllocator> _cacheAllocators = [];
|
||||
|
||||
private static readonly List<CacheEntry> _cacheEntries = new();
|
||||
|
||||
@ -42,31 +42,42 @@ namespace ARMeilleure.Translation.Cache
|
||||
|
||||
public static void Initialize(IJitMemoryAllocator allocator)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
// JitUnwindWindows.RemoveFunctionTableHandler(
|
||||
// _jitRegions[0].Pointer);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _jitRegions.Count; i++)
|
||||
{
|
||||
_jitRegions[i].Dispose();
|
||||
}
|
||||
|
||||
_jitRegions.Clear();
|
||||
_cacheAllocators.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
||||
|
||||
|
||||
_jitRegions.Add(firstRegion);
|
||||
_activeRegionIndex = 0;
|
||||
|
||||
var firstRegion = new ReservedRegion(allocator, CacheSize);
|
||||
_jitRegions.Add(firstRegion);
|
||||
|
||||
CacheMemoryAllocator firstCacheAllocator = new(CacheSize);
|
||||
_cacheAllocators.Add(firstCacheAllocator);
|
||||
|
||||
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsIOS())
|
||||
{
|
||||
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
|
||||
}
|
||||
|
||||
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
JitUnwindWindows.InstallFunctionTableHandler(
|
||||
@ -162,7 +173,7 @@ namespace ARMeilleure.Translation.Cache
|
||||
|
||||
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
|
||||
{
|
||||
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
|
||||
_cacheAllocators[_activeRegionIndex].Free(funcOffset, AlignCodeSize(entry.Size));
|
||||
_cacheEntries.RemoveAt(entryIndex);
|
||||
}
|
||||
|
||||
@ -202,16 +213,12 @@ namespace ARMeilleure.Translation.Cache
|
||||
alignment = 0x4000;
|
||||
}
|
||||
|
||||
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
|
||||
{
|
||||
int allocOffset = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||
int allocOffset = _cacheAllocators[_activeRegionIndex].Allocate(ref codeSize, alignment);
|
||||
|
||||
if (allocOffset >= 0)
|
||||
{
|
||||
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||
_activeRegionIndex = i;
|
||||
return allocOffset;
|
||||
}
|
||||
if (allocOffset >= 0)
|
||||
{
|
||||
_jitRegions[_activeRegionIndex].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
|
||||
return allocOffset;
|
||||
}
|
||||
|
||||
int exhaustedRegion = _activeRegionIndex;
|
||||
@ -221,9 +228,11 @@ namespace ARMeilleure.Translation.Cache
|
||||
|
||||
int newRegionNumber = _activeRegionIndex;
|
||||
|
||||
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
|
||||
Logger.Info?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {_activeRegionIndex} ({((long)(_activeRegionIndex + 1) * CacheSize)} Total Allocation).");
|
||||
|
||||
int allocOffsetNew = _cacheAllocator.Allocate(ref codeSize, alignment);
|
||||
_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!");
|
||||
|
@ -674,6 +674,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 = fast;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -761,6 +765,14 @@
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
@ -822,6 +834,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 = fast;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -909,6 +925,14 @@
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
"$(PROJECT_DIR)/MeloNX/Dependencies/Dynamic\\ Libraries",
|
||||
);
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stossy11.MeloNX;
|
||||
|
Binary file not shown.
@ -48,13 +48,21 @@ void install_firmware(const char* inputPtr);
|
||||
|
||||
char* installed_firmware_version();
|
||||
|
||||
void set_native_window(void *layerPtr);
|
||||
|
||||
void stop_emulation();
|
||||
|
||||
void initialize();
|
||||
|
||||
int main_ryujinx_sdl(int argc, char **argv);
|
||||
|
||||
int get_current_fps();
|
||||
|
||||
void initialize();
|
||||
void touch_began(float x, float y, int index);
|
||||
|
||||
void touch_moved(float x, float y, int index);
|
||||
|
||||
void touch_ended(int index);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
@ -7,6 +7,19 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
@_silgen_name("csops")
|
||||
func csops(pid: Int32, ops: Int32, useraddr: UnsafeMutableRawPointer?, usersize: Int32) -> Int32
|
||||
|
||||
func isJITEnabled() -> Bool {
|
||||
var flags: Int = 0
|
||||
|
||||
if checkAppEntitlement("dynamic-codesigning") {
|
||||
return allocateTest()
|
||||
}
|
||||
|
||||
return csops(pid: getpid(), ops: 0, useraddr: &flags, usersize: Int32(MemoryLayout.size(ofValue: flags))) == 0 && (flags & Int(CS_DEBUGGED)) != 0 ? allocateTest() : false
|
||||
}
|
||||
|
||||
func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
|
||||
var region: vm_address_t = vm_address_t(UInt(bitPattern: address))
|
||||
var regionSize: vm_size_t = 0
|
||||
@ -27,8 +40,7 @@ func checkMemoryPermissions(at address: UnsafeRawPointer) -> Bool {
|
||||
|
||||
return info.protection & VM_PROT_EXECUTE != 0
|
||||
}
|
||||
|
||||
func isJITEnabled() -> Bool {
|
||||
func allocateTest() -> Bool {
|
||||
let pageSize = sysconf(_SC_PAGESIZE)
|
||||
let code: [UInt32] = [0x52800540, 0xD65F03C0]
|
||||
|
||||
@ -40,7 +52,6 @@ func isJITEnabled() -> Bool {
|
||||
munmap(jitMemory, pageSize)
|
||||
}
|
||||
|
||||
|
||||
memcpy(jitMemory, code, code.count)
|
||||
|
||||
if mprotect(jitMemory, pageSize, PROT_READ | PROT_EXEC) != 0 {
|
||||
|
@ -11,6 +11,7 @@ func enableJITEB() {
|
||||
guard let bundleID = Bundle.main.bundleIdentifier else {
|
||||
return
|
||||
}
|
||||
|
||||
let address = URL(string: "http://[fd00::]:9172/launch_app/\(bundleID)")!
|
||||
|
||||
let task = URLSession.shared.dataTask(with: address) { data, response, error in
|
||||
|
@ -91,10 +91,10 @@ class NativeController: Hashable {
|
||||
guard let gamepad = nativeController.extendedGamepad
|
||||
else { return }
|
||||
|
||||
setupButtonChangeListener(gamepad.buttonA, for: .B)
|
||||
setupButtonChangeListener(gamepad.buttonB, for: .A)
|
||||
setupButtonChangeListener(gamepad.buttonX, for: .Y)
|
||||
setupButtonChangeListener(gamepad.buttonY, for: .X)
|
||||
setupButtonChangeListener(gamepad.buttonA, for: .A)
|
||||
setupButtonChangeListener(gamepad.buttonB, for: .B)
|
||||
setupButtonChangeListener(gamepad.buttonX, for: .X)
|
||||
setupButtonChangeListener(gamepad.buttonY, for: .Y)
|
||||
|
||||
setupButtonChangeListener(gamepad.dpad.up, for: .dPadUp)
|
||||
setupButtonChangeListener(gamepad.dpad.down, for: .dPadDown)
|
||||
|
@ -45,7 +45,9 @@ class VirtualController {
|
||||
},
|
||||
Rumble: { userdata, lowFreq, highFreq in
|
||||
print("Rumble with \(lowFreq), \(highFreq)")
|
||||
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq))
|
||||
}
|
||||
return 0
|
||||
},
|
||||
RumbleTriggers: { userdata, leftRumble, rightRumble in
|
||||
@ -80,7 +82,6 @@ class VirtualController {
|
||||
|
||||
static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) {
|
||||
do {
|
||||
// Low-frequency haptic pattern
|
||||
let lowFreqPattern = try CHHapticPattern(events: [
|
||||
CHHapticEvent(eventType: .hapticTransient, parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq),
|
||||
@ -88,7 +89,7 @@ class VirtualController {
|
||||
], relativeTime: 0, duration: 0.2)
|
||||
], parameters: [])
|
||||
|
||||
// High-frequency haptic pattern
|
||||
|
||||
let highFreqPattern = try CHHapticPattern(events: [
|
||||
CHHapticEvent(eventType: .hapticTransient, parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq),
|
||||
@ -96,12 +97,9 @@ class VirtualController {
|
||||
], relativeTime: 0.2, duration: 0.2)
|
||||
], parameters: [])
|
||||
|
||||
// Mutable engine
|
||||
var engine = engine
|
||||
|
||||
// If no engine passed, use device engine
|
||||
if engine == nil {
|
||||
// Create and start the haptic engine
|
||||
if hapticEngine == nil {
|
||||
hapticEngine = try CHHapticEngine()
|
||||
try hapticEngine?.start()
|
||||
@ -114,13 +112,11 @@ class VirtualController {
|
||||
return print("Error creating haptic patterns: hapticEngine is nil")
|
||||
}
|
||||
|
||||
// Create and play the low-frequency player
|
||||
let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern)
|
||||
try lowFreqPlayer.start(atTime: 0)
|
||||
|
||||
// Create and play the high-frequency player after a short delay
|
||||
|
||||
let highFreqPlayer = try engine.makePlayer(with: highFreqPattern)
|
||||
try highFreqPlayer.start(atTime: 0.2)
|
||||
try highFreqPlayer.start(atTime: 0)
|
||||
|
||||
} catch {
|
||||
print("Error creating haptic patterns: \(error)")
|
||||
|
@ -1,104 +0,0 @@
|
||||
//
|
||||
// Untitled.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 28/11/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import GameController
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
var theWindow: UIWindow? = nil
|
||||
extension UIWindow {
|
||||
// Makes the SDLWindow use the current WindowScene instead of making its own window.
|
||||
// Also waits for the window to append the on-screen controller
|
||||
@objc func wdb_makeKeyAndVisible() {
|
||||
let enabled = UserDefaults.standard.bool(forKey: "oldWindowCode")
|
||||
|
||||
if #unavailable(iOS 17.0), enabled {
|
||||
self.windowScene = (UIApplication.shared.connectedScenes.first! as! UIWindowScene)
|
||||
}
|
||||
|
||||
self.wdb_makeKeyAndVisible()
|
||||
theWindow = self
|
||||
|
||||
if #available(iOS 17, *) {
|
||||
Ryujinx.shared.repeatuntilfindLayer()
|
||||
} else if UserDefaults.standard.bool(forKey: "isVirtualController") && enabled {
|
||||
waitForController()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iOS 16 and below Only
|
||||
|
||||
var hostingController: UIHostingController<ControllerView>?
|
||||
func waitForController() {
|
||||
guard let window = theWindow else { return }
|
||||
|
||||
// Function to search for an existing UIHostingController with ControllerView
|
||||
func findGCControllerView(in view: UIView) -> UIHostingController<ControllerView>? {
|
||||
if let hostingVC = view.next as? UIHostingController<ControllerView> {
|
||||
return hostingVC
|
||||
}
|
||||
|
||||
for subview in view.subviews {
|
||||
if let found = findGCControllerView(in: subview) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let controllerView = ControllerView()
|
||||
let newHostingController = UIHostingController(rootView: controllerView)
|
||||
|
||||
hostingController = newHostingController
|
||||
|
||||
let containerView = newHostingController.view!
|
||||
containerView.backgroundColor = .clear
|
||||
containerView.frame = window.bounds
|
||||
containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
// Timer for controller
|
||||
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
|
||||
if findGCControllerView(in: window) == nil {
|
||||
// Adds Virtual Controller Subview
|
||||
window.addSubview(containerView)
|
||||
window.bringSubviewToFront(containerView)
|
||||
|
||||
if let sdlWindow = SDL_GetWindowFromID(1) {
|
||||
SDL_SetWindowPosition(sdlWindow, 0, 0)
|
||||
}
|
||||
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TransparentHostingContainerView: UIView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
// Check if the point is within the subviews of this container
|
||||
let view = super.hitTest(point, with: event)
|
||||
print(view)
|
||||
|
||||
// Return nil if the touch is outside visible content (passes through to views below)
|
||||
return view === self ? nil : view
|
||||
}
|
||||
}
|
||||
|
||||
// Patches makeKeyAndVisible to wdb_makeKeyAndVisible
|
||||
func patchMakeKeyAndVisible() {
|
||||
let uiwindowClass = UIWindow.self
|
||||
if let m1 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.makeKeyAndVisible)),
|
||||
let m2 = class_getInstanceMethod(uiwindowClass, #selector(UIWindow.wdb_makeKeyAndVisible)) {
|
||||
method_exchangeImplementations(m1, m2)
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,8 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import GameController
|
||||
import MetalKit
|
||||
import Metal
|
||||
|
||||
struct Controller: Identifiable, Hashable {
|
||||
var id: String
|
||||
@ -37,7 +39,8 @@ class Ryujinx {
|
||||
@Published var controllerMap: [Controller] = []
|
||||
@Published var metalLayer: CAMetalLayer? = nil
|
||||
@Published var firmwareversion = "0"
|
||||
@Published var emulationUIView = UIView()
|
||||
@Published var emulationUIView: MeloMTKView? = nil
|
||||
@Published var config: Ryujinx.Configuration? = nil
|
||||
@Published var games: [Game] = []
|
||||
|
||||
@Published var defMLContentSize: CGFloat?
|
||||
@ -140,9 +143,11 @@ class Ryujinx {
|
||||
throw RyujinxError.alreadyRunning
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
self.config = config
|
||||
|
||||
RunLoop.current.perform {
|
||||
RunLoop.current.perform { [self] in
|
||||
|
||||
isRunning = true
|
||||
|
||||
let url = URL(string: config.gamepath)
|
||||
|
||||
@ -156,15 +161,17 @@ class Ryujinx {
|
||||
var argvPtrs = cArgs
|
||||
|
||||
// Start the emulation
|
||||
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
|
||||
|
||||
if result != 0 {
|
||||
self.isRunning = false
|
||||
if let accessing, accessing {
|
||||
url!.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
if isRunning {
|
||||
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
|
||||
|
||||
throw RyujinxError.executionError(code: result)
|
||||
if result != 0 {
|
||||
self.isRunning = false
|
||||
if let accessing, accessing {
|
||||
url!.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
throw RyujinxError.executionError(code: result)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
self.isRunning = false
|
||||
@ -180,6 +187,11 @@ class Ryujinx {
|
||||
}
|
||||
|
||||
isRunning = false
|
||||
|
||||
self.emulationUIView = nil
|
||||
self.metalLayer = nil
|
||||
|
||||
stop_emulation()
|
||||
}
|
||||
|
||||
var running: Bool {
|
||||
@ -334,13 +346,19 @@ class Ryujinx {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apped any additional arguments
|
||||
|
||||
args.append(contentsOf: config.additionalArgs)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func checkIfKeysImported() -> Bool {
|
||||
let keysDirectory = URL.documentsDirectory.appendingPathComponent("system")
|
||||
let keysFile = keysDirectory.appendingPathComponent("prod.keys")
|
||||
|
||||
return FileManager.default.fileExists(atPath: keysFile.path)
|
||||
}
|
||||
|
||||
func fetchFirmwareVersion() -> String {
|
||||
do {
|
||||
let firmwareVersionPointer = installed_firmware_version()
|
||||
@ -489,15 +507,11 @@ class Ryujinx {
|
||||
let layer = self.getMetalLayer(nil)
|
||||
|
||||
if layer != nil {
|
||||
DispatchQueue.main.async {
|
||||
self.metalLayer = layer
|
||||
}
|
||||
self.metalLayer = layer
|
||||
break
|
||||
}
|
||||
|
||||
Thread.sleep(forTimeInterval: 0.1)
|
||||
try await Task.sleep(nanoseconds: 100_000_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
//
|
||||
// MetalView.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 09/02/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MetalKit
|
||||
|
||||
struct MetalView: UIViewRepresentable {
|
||||
|
||||
var airplay: Bool = Air.shared.connected // just in case :3
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let metalLayer = Ryujinx.shared.metalLayer!
|
||||
|
||||
var view = UIView()
|
||||
|
||||
metalLayer.frame = view.bounds
|
||||
if airplay {
|
||||
metalLayer.contentsScale = view.contentScaleFactor
|
||||
} else {
|
||||
Ryujinx.shared.emulationUIView.contentScaleFactor = metalLayer.contentsScale // Right size and Fix Touch :3
|
||||
}
|
||||
|
||||
Ryujinx.shared.emulationUIView = view
|
||||
|
||||
if !Ryujinx.shared.emulationUIView.subviews.contains(where: { $0 == metalLayer }) {
|
||||
Ryujinx.shared.emulationUIView.layer.addSublayer(metalLayer)
|
||||
}
|
||||
|
||||
return Ryujinx.shared.emulationUIView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
// nothin
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
//
|
||||
// LogEntry.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 09/02/2025.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LogEntry: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
let text: String
|
||||
|
||||
static func == (lhs: LogEntry, rhs: LogEntry) -> Bool {
|
||||
return lhs.id == rhs.id && lhs.text == rhs.text
|
||||
}
|
||||
}
|
||||
|
||||
struct LogViewer: View {
|
||||
@State private var logs: [LogEntry] = []
|
||||
@State private var latestLogFilePath: String?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
ForEach(logs) { log in
|
||||
Text(log.text)
|
||||
.padding(4)
|
||||
.background(Color.black.opacity(0.7))
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(8)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
.animation(.easeOut(duration: 2), value: logs)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.onAppear {
|
||||
findNewestLogFile()
|
||||
}
|
||||
}
|
||||
|
||||
func findNewestLogFile() {
|
||||
let logsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("logs")
|
||||
|
||||
guard let directory = logsDirectory else { return }
|
||||
|
||||
do {
|
||||
let logFiles = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.contentModificationDateKey], options: .skipsHiddenFiles)
|
||||
|
||||
// Sort files by modification date (newest first)
|
||||
let sortedFiles = logFiles.sorted {
|
||||
(try? $0.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? Date.distantPast >
|
||||
(try? $1.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? Date.distantPast
|
||||
}
|
||||
|
||||
if let newestLogFile = sortedFiles.first {
|
||||
latestLogFilePath = newestLogFile.path
|
||||
startReadingLogFile()
|
||||
}
|
||||
} catch {
|
||||
print("Error reading log files: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func startReadingLogFile() {
|
||||
guard let path = latestLogFilePath else { return }
|
||||
let fileHandle = try? FileHandle(forReadingAtPath: path)
|
||||
fileHandle?.seekToEndOfFile()
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .NSFileHandleDataAvailable, object: fileHandle, queue: .main) { _ in
|
||||
if let data = fileHandle?.availableData, !data.isEmpty {
|
||||
if let logLine = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) {
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
logs.append(LogEntry(text: logLine))
|
||||
}
|
||||
// Remove old logs after a delay
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
withAnimation {
|
||||
removelogfirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fileHandle?.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
|
||||
fileHandle?.waitForDataInBackgroundAndNotify()
|
||||
}
|
||||
|
||||
func removelogfirst() {
|
||||
logs.removeFirst()
|
||||
}
|
||||
}
|
@ -44,26 +44,37 @@ struct ContentView: View {
|
||||
@State var quits: Bool = false
|
||||
@AppStorage("MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS") var mVKPreFillBuffer: Bool = true
|
||||
@AppStorage("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS") var syncqsubmits: Bool = true
|
||||
@AppStorage("ignoreJIT") var ignoreJIT: Bool = false
|
||||
|
||||
// Loading Animation
|
||||
@AppStorage("showlogsloading") var showlogsloading: Bool = true
|
||||
@State private var clumpOffset: CGFloat = -100
|
||||
private let clumpWidth: CGFloat = 100
|
||||
private let animationDuration: Double = 1.0
|
||||
@State private var isAnimating = false
|
||||
@State var isLoading = true
|
||||
@State var jitNotEnabled = false
|
||||
|
||||
// MARK: - Initialization
|
||||
init() {
|
||||
let defaultConfig = loadSettings() ?? Ryujinx.Configuration(gamepath: "")
|
||||
_config = State(initialValue: defaultConfig)
|
||||
var defaultConfig = loadSettings()
|
||||
if defaultConfig == nil {
|
||||
saveSettings(config: .init(gamepath: ""))
|
||||
|
||||
defaultConfig = loadSettings()
|
||||
}
|
||||
|
||||
|
||||
_config = State(initialValue: defaultConfig!)
|
||||
|
||||
let defaultSettings: [MoltenVKSettings] = [ // Default MoltenVK Settings.
|
||||
MoltenVKSettings(string: "MVK_USE_METAL_PRIVATE_API", value: "1"),
|
||||
MoltenVKSettings(string: "MVK_CONFIG_USE_METAL_PRIVATE_API", value: "1"),
|
||||
MoltenVKSettings(string: "MVK_DEBUG", value: "0"),
|
||||
MoltenVKSettings(string: "MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", value: "0"),
|
||||
MoltenVKSettings(string: "MVK_CONFIG_PREFILL_METAL_COMMAND_BUFFERS", value: "0"),
|
||||
// Uses more ram but makes performance higher, may add an option in settings to change or enable / disable this value (default 64)
|
||||
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "128"),
|
||||
MoltenVKSettings(string: "MVK_CONFIG_MAX_ACTIVE_METAL_COMMAND_BUFFERS_PER_QUEUE", value: "512"),
|
||||
]
|
||||
|
||||
_settings = State(initialValue: defaultSettings)
|
||||
@ -73,38 +84,64 @@ struct ContentView: View {
|
||||
|
||||
// MARK: - Body
|
||||
var body: some View {
|
||||
if game != nil, quits == false {
|
||||
if isLoading {
|
||||
if Air.shared.connected {
|
||||
Text("")
|
||||
.onAppear() {
|
||||
Air.play(AnyView(emulationView))
|
||||
}
|
||||
} else {
|
||||
ZStack {
|
||||
emulationView
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is when the game starts to stop the animation
|
||||
if game != nil, !jitNotEnabled {
|
||||
// This is when the game starts to stop the animation
|
||||
ZStack {
|
||||
if #available(iOS 16, *) {
|
||||
EmulationView()
|
||||
EmulationView(startgame: $game)
|
||||
.persistentSystemOverlays(.hidden)
|
||||
.onAppear() {
|
||||
isAnimating = false
|
||||
}
|
||||
} else {
|
||||
EmulationView()
|
||||
.onAppear() {
|
||||
isAnimating = false
|
||||
}
|
||||
EmulationView(startgame: $game)
|
||||
}
|
||||
|
||||
if isLoading {
|
||||
ZStack {
|
||||
Color.black
|
||||
.opacity(0.8)
|
||||
emulationView
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
} else if game != nil, ignoreJIT {
|
||||
ZStack {
|
||||
if #available(iOS 16, *) {
|
||||
EmulationView(startgame: $game)
|
||||
.persistentSystemOverlays(.hidden)
|
||||
} else {
|
||||
EmulationView(startgame: $game)
|
||||
}
|
||||
|
||||
if isLoading {
|
||||
ZStack {
|
||||
Color.black
|
||||
.opacity(0.8)
|
||||
emulationView
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
}
|
||||
} else if game != nil {
|
||||
Text("")
|
||||
.sheet(isPresented: $jitNotEnabled) {
|
||||
JITPopover() {
|
||||
jitNotEnabled = false
|
||||
}
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
} else {
|
||||
// This is the main menu view that includes the Settings and the Game Selector
|
||||
mainMenuView
|
||||
.onAppear() {
|
||||
quits = false
|
||||
|
||||
loadSettings()
|
||||
|
||||
isLoading = true
|
||||
|
||||
initControllerObservers() // This initializes the Controller Observers that refreshes the controller list when a new controller connecvts.
|
||||
}
|
||||
@ -204,12 +241,14 @@ struct ContentView: View {
|
||||
if get_current_fps() != 0 {
|
||||
withAnimation {
|
||||
isLoading = false
|
||||
|
||||
isAnimating = false
|
||||
}
|
||||
|
||||
isAnimating = false
|
||||
|
||||
|
||||
timer.invalidate()
|
||||
}
|
||||
print(get_current_fps())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -224,6 +263,11 @@ struct ContentView: View {
|
||||
y: screenGeometry.size.height * 0.5
|
||||
)
|
||||
}
|
||||
|
||||
if showlogsloading {
|
||||
LogFileView(isfps: true)
|
||||
.frame(alignment: .topLeading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,9 +291,9 @@ struct ContentView: View {
|
||||
}
|
||||
))
|
||||
|
||||
let isJIT = isJITEnabled()
|
||||
if !isJIT {
|
||||
useTrollStore ? askForJIT() : enableJITEB()
|
||||
jitNotEnabled = !isJITEnabled()
|
||||
if jitNotEnabled {
|
||||
useTrollStore ? askForJIT() : jitStreamerEB ? enableJITEB() : print("no JIT")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -265,7 +309,6 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private func setupEmulation() {
|
||||
patchMakeKeyAndVisible()
|
||||
isVCA = (currentControllers.first(where: { $0 == onscreencontroller }) != nil)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
@ -319,6 +362,7 @@ struct ContentView: View {
|
||||
config.inputids.append("0")
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
try Ryujinx.shared.start(with: config)
|
||||
} catch {
|
||||
@ -336,21 +380,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Functions
|
||||
func loadSettings() -> Ryujinx.Configuration? {
|
||||
guard let jsonString = UserDefaults.standard.string(forKey: "config"),
|
||||
let data = jsonString.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
return try JSONDecoder().decode(Ryujinx.Configuration.self, from: data)
|
||||
} catch {
|
||||
print("Failed to load settings: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Array {
|
||||
@inlinable public mutating func mutableForEach(_ body: (inout Element) throws -> Void) rethrows {
|
||||
for index in self.indices {
|
@ -11,29 +11,10 @@ import SwiftUIJoystick
|
||||
import CoreMotion
|
||||
|
||||
struct ControllerView: View {
|
||||
|
||||
@AppStorage("performacehud") var performacehud: Bool = false
|
||||
@AppStorage("quit") var quit: Bool = false
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
|
||||
VStack {
|
||||
if performacehud {
|
||||
HStack {
|
||||
|
||||
PerformanceOverlayView()
|
||||
|
||||
Spacer()
|
||||
|
||||
// Button("Stop emulation") {
|
||||
// DispatchQueue.main.async {
|
||||
// stop_emulation()
|
||||
// quit = true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
VStack {
|
||||
@ -67,21 +48,6 @@ struct ControllerView: View {
|
||||
} else {
|
||||
// could be landscape
|
||||
VStack {
|
||||
if performacehud {
|
||||
HStack {
|
||||
PerformanceOverlayView()
|
||||
|
||||
Spacer()
|
||||
|
||||
// Button("Stop emulation") {
|
||||
// DispatchQueue.main.async {
|
||||
// stop_emulation()
|
||||
// quit = true
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
VStack {
|
@ -9,15 +9,22 @@ import SwiftUI
|
||||
|
||||
// Emulation View
|
||||
struct EmulationView: View {
|
||||
@AppStorage("performacehud") var performacehud: Bool = false
|
||||
@AppStorage("isVirtualController") var isVCA: Bool = true
|
||||
@AppStorage("showScreenShotButton") var ssb: Bool = false
|
||||
@AppStorage("showlogsgame") var showlogsgame: Bool = false
|
||||
|
||||
@State var isPresentedThree: Bool = false
|
||||
@State var isAirplaying = Air.shared.connected
|
||||
@Binding var startgame: Game?
|
||||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if isAirplaying {
|
||||
Text("")
|
||||
TouchView()
|
||||
.ignoresSafeArea()
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.onAppear {
|
||||
Air.play(AnyView(MetalView().ignoresSafeArea()))
|
||||
}
|
||||
@ -33,16 +40,35 @@ struct EmulationView: View {
|
||||
ControllerView() // Virtual Controller
|
||||
}
|
||||
|
||||
|
||||
if ssb {
|
||||
Group {
|
||||
VStack {
|
||||
Group {
|
||||
VStack {
|
||||
HStack {
|
||||
if performacehud, !showlogsgame {
|
||||
PerformanceOverlayView()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if performacehud, showlogsgame {
|
||||
PerformanceOverlayView()
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
if showlogsgame, get_current_fps() != 0 {
|
||||
LogFileView(isfps: false)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if ssb {
|
||||
HStack {
|
||||
|
||||
Button {
|
||||
if let screenshot = Ryujinx.shared.emulationUIView.screenshot() {
|
||||
if let screenshot = Ryujinx.shared.emulationUIView?.screenshot() {
|
||||
UIImageWriteToSavedPhotosAlbum(screenshot, nil, nil, nil)
|
||||
}
|
||||
} label: {
|
||||
@ -52,9 +78,12 @@ struct EmulationView: View {
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
//
|
||||
// MeloMTKView.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 03/03/2025.
|
||||
//
|
||||
|
||||
import MetalKit
|
||||
import UIKit
|
||||
|
||||
class MeloMTKView: MTKView {
|
||||
|
||||
private var activeTouches: [UITouch] = []
|
||||
private var ignoredTouches: Set<UITouch> = []
|
||||
|
||||
private let baseWidth: CGFloat = 1280
|
||||
private let baseHeight: CGFloat = 720
|
||||
private var aspectRatio: AspectRatio = .fixed16x9
|
||||
|
||||
func setAspectRatio(_ ratio: AspectRatio) {
|
||||
self.aspectRatio = ratio
|
||||
}
|
||||
|
||||
private func scaleToTargetResolution(_ location: CGPoint) -> CGPoint? {
|
||||
let viewWidth = self.frame.width
|
||||
let viewHeight = self.frame.height
|
||||
|
||||
var scaleX: CGFloat
|
||||
var scaleY: CGFloat
|
||||
var offsetX: CGFloat = 0
|
||||
var offsetY: CGFloat = 0
|
||||
|
||||
var targetAspect: CGFloat
|
||||
|
||||
switch aspectRatio {
|
||||
case .fixed4x3:
|
||||
targetAspect = 4.0 / 3.0
|
||||
case .fixed16x9:
|
||||
targetAspect = 16.0 / 9.0
|
||||
case .fixed16x10:
|
||||
targetAspect = 16.0 / 10.0
|
||||
case .fixed21x9:
|
||||
targetAspect = 21.0 / 9.0
|
||||
case .fixed32x9:
|
||||
targetAspect = 32.0 / 9.0
|
||||
case .stretched:
|
||||
scaleX = baseWidth / viewWidth
|
||||
scaleY = baseHeight / viewHeight
|
||||
|
||||
let adjustedX = location.x
|
||||
let adjustedY = location.y
|
||||
|
||||
let scaledX = max(0, min(adjustedX * scaleX, baseWidth))
|
||||
let scaledY = max(0, min(adjustedY * scaleY, baseHeight))
|
||||
|
||||
return CGPoint(x: scaledX, y: scaledY)
|
||||
}
|
||||
|
||||
let viewAspect = viewWidth / viewHeight
|
||||
|
||||
if viewAspect > targetAspect {
|
||||
let scaledWidth = viewHeight * targetAspect
|
||||
offsetX = (viewWidth - scaledWidth) / 2
|
||||
scaleX = baseWidth / scaledWidth
|
||||
scaleY = baseHeight / viewHeight
|
||||
} else {
|
||||
let scaledHeight = viewWidth / targetAspect
|
||||
offsetY = (viewHeight - scaledHeight) / 2
|
||||
scaleX = baseWidth / viewWidth
|
||||
scaleY = baseHeight / scaledHeight
|
||||
}
|
||||
|
||||
if location.x < offsetX || location.x > (viewWidth - offsetX) ||
|
||||
location.y < offsetY || location.y > (viewHeight - offsetY) {
|
||||
return nil
|
||||
}
|
||||
|
||||
let adjustedX = location.x - offsetX
|
||||
let adjustedY = location.y - offsetY
|
||||
|
||||
let scaledX = max(0, min(adjustedX * scaleX, baseWidth))
|
||||
let scaledY = max(0, min(adjustedY * scaleY, baseHeight))
|
||||
|
||||
return CGPoint(x: scaledX, y: scaledY)
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
||||
|
||||
for touch in touches {
|
||||
let location = touch.location(in: self)
|
||||
if scaleToTargetResolution(location) == nil {
|
||||
ignoredTouches.insert(touch)
|
||||
continue
|
||||
}
|
||||
|
||||
activeTouches.append(touch)
|
||||
let index = activeTouches.firstIndex(of: touch)!
|
||||
|
||||
let scaledLocation = scaleToTargetResolution(location)!
|
||||
print("Touch began at: \(scaledLocation) and \(self.aspectRatio)")
|
||||
touch_began(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
||||
|
||||
for touch in touches {
|
||||
if ignoredTouches.contains(touch) {
|
||||
ignoredTouches.remove(touch)
|
||||
continue
|
||||
}
|
||||
|
||||
if let index = activeTouches.firstIndex(of: touch) {
|
||||
activeTouches.remove(at: index)
|
||||
|
||||
print("Touch ended for index \(index)")
|
||||
touch_ended(Int32(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
setAspectRatio(Ryujinx.shared.config?.aspectRatio ?? .fixed16x9)
|
||||
|
||||
for touch in touches {
|
||||
if ignoredTouches.contains(touch) {
|
||||
continue
|
||||
}
|
||||
|
||||
let location = touch.location(in: self)
|
||||
guard let scaledLocation = scaleToTargetResolution(location) else {
|
||||
if let index = activeTouches.firstIndex(of: touch) {
|
||||
activeTouches.remove(at: index)
|
||||
print("Touch left active area, removed index \(index)")
|
||||
touch_ended(Int32(index))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if let index = activeTouches.firstIndex(of: touch) {
|
||||
print("Touch moved to: \(scaledLocation)")
|
||||
touch_moved(Float(scaledLocation.x), Float(scaledLocation.y), Int32(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
//
|
||||
// MetalView.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 09/02/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MetalKit
|
||||
|
||||
struct MetalView: UIViewRepresentable {
|
||||
|
||||
var airplay: Bool = Air.shared.connected // just in case :3
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
|
||||
if Ryujinx.shared.emulationUIView == nil {
|
||||
var view = MeloMTKView()
|
||||
|
||||
guard let metalLayer = view.layer as? CAMetalLayer else {
|
||||
fatalError("[Swift] Error: MTKView's layer is not a CAMetalLayer")
|
||||
}
|
||||
|
||||
metalLayer.device = MTLCreateSystemDefaultDevice()
|
||||
|
||||
let layerPtr = Unmanaged.passUnretained(metalLayer).toOpaque()
|
||||
set_native_window(layerPtr)
|
||||
|
||||
Ryujinx.shared.emulationUIView = view
|
||||
|
||||
|
||||
Ryujinx.shared.metalLayer = metalLayer
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
let uiview = UIView()
|
||||
|
||||
uiview.layer.addSublayer(Ryujinx.shared.metalLayer!)
|
||||
|
||||
uiview.contentScaleFactor = Ryujinx.shared.metalLayer!.contentsScale
|
||||
|
||||
return uiview
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
// nothin
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
//
|
||||
// TouchView.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 05/03/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MetalKit
|
||||
|
||||
struct TouchView: UIViewRepresentable {
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
var view = MeloMTKView()
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {}
|
||||
}
|
@ -134,7 +134,9 @@ struct GameLibraryView: View {
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
isSelectingGameFile.toggle()
|
||||
isSelectingGameFile = true
|
||||
|
||||
isImporting = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
@ -171,16 +173,17 @@ struct GameLibraryView: View {
|
||||
} label: {
|
||||
Text("Mii Maker")
|
||||
}
|
||||
Button {
|
||||
DispatchQueue.main.async {
|
||||
isImporting.toggle()
|
||||
}
|
||||
} label: {
|
||||
Text("Open game from system")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
isSelectingGameFile = false
|
||||
|
||||
isImporting = true
|
||||
} label: {
|
||||
Text("Open Game")
|
||||
}
|
||||
|
||||
Button {
|
||||
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
var sharedurl = documentsUrl.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
|
||||
@ -211,62 +214,64 @@ struct GameLibraryView: View {
|
||||
.onChange(of: searchText) { _ in
|
||||
isSearching = !searchText.isEmpty
|
||||
}
|
||||
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.zip, .folder, .nsp, .xci]) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
print("Failed to access security-scoped resource")
|
||||
return
|
||||
}
|
||||
defer { url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
do {
|
||||
let handle = try FileHandle(forReadingFrom: url)
|
||||
let fileExtension = (url.pathExtension as NSString).utf8String
|
||||
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
||||
|
||||
var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||
|
||||
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
startemu = game
|
||||
.fileImporter(isPresented: $isImporting, allowedContentTypes: [.folder, .nsp, .xci, .zip, .item]) { result in
|
||||
if isSelectingGameFile {
|
||||
switch result {
|
||||
case .success(let url):
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
print("Failed to access security-scoped resource")
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
|
||||
case .failure(let err):
|
||||
print("File import failed: \(err.localizedDescription)")
|
||||
}
|
||||
}
|
||||
.fileImporter(isPresented: $isSelectingGameFile, allowedContentTypes: [.nsp, .xci, .zip, .folder]) { result in
|
||||
switch result {
|
||||
case .success(let url):
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
print("Failed to access security-scoped resource")
|
||||
return
|
||||
}
|
||||
defer { url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
|
||||
defer { url.stopAccessingSecurityScopedResource() }
|
||||
|
||||
if !fileManager.fileExists(atPath: romsDirectory.path) {
|
||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let romsDirectory = documentsDirectory.appendingPathComponent("roms")
|
||||
|
||||
if !fileManager.fileExists(atPath: romsDirectory.path) {
|
||||
try fileManager.createDirectory(at: romsDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
|
||||
try fileManager.copyItem(at: url, to: destinationURL)
|
||||
|
||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||
} catch {
|
||||
print("Error copying game file: \(error)")
|
||||
}
|
||||
case .failure(let err):
|
||||
print("File import failed: \(err.localizedDescription)")
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
switch result {
|
||||
case .success(let url):
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
print("Failed to access security-scoped resource")
|
||||
return
|
||||
}
|
||||
|
||||
let destinationURL = romsDirectory.appendingPathComponent(url.lastPathComponent)
|
||||
try fileManager.copyItem(at: url, to: destinationURL)
|
||||
do {
|
||||
let handle = try FileHandle(forReadingFrom: url)
|
||||
let fileExtension = (url.pathExtension as NSString).utf8String
|
||||
let extensionPtr = UnsafeMutablePointer<CChar>(mutating: fileExtension)
|
||||
|
||||
var gameInfo = get_game_info(handle.fileDescriptor, extensionPtr)
|
||||
|
||||
let game = Game.convertGameInfoToGame(gameInfo: gameInfo, url: url)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
startemu = game
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
|
||||
Ryujinx.shared.games = Ryujinx.shared.loadGames()
|
||||
} catch {
|
||||
print("Error copying game file: \(error)")
|
||||
case .failure(let err):
|
||||
print("File import failed: \(err.localizedDescription)")
|
||||
}
|
||||
case .failure(let err):
|
||||
print("File import failed: \(err.localizedDescription)")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isSelectingGameUpdate) {
|
44
src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift
Normal file
44
src/MeloNX/MeloNX/App/Views/Main/JIT/JITPopover.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// JITPopover.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 05/03/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct JITPopover: View {
|
||||
var onJITEnabled: () -> Void
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@State var isJIT: Bool = false
|
||||
var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "cpu.fill")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
Text("Waiting for JIT")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("JIT (Just-In-Time) compilation allows MeloNX to run code at as fast as possible by translating it dynamically. This is necessary for running this emulator.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
.onAppear {
|
||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
|
||||
isJIT = isJITEnabled()
|
||||
|
||||
|
||||
if isJIT {
|
||||
dismiss()
|
||||
onJITEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift
Normal file
117
src/MeloNX/MeloNX/App/Views/Main/Logging/Logs.swift
Normal file
@ -0,0 +1,117 @@
|
||||
//
|
||||
// LogEntry.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 09/02/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LogFileView: View {
|
||||
@State private var logs: [String] = []
|
||||
@State private var showingLogs = false
|
||||
|
||||
public var isfps: Bool
|
||||
|
||||
private let fileManager = FileManager.default
|
||||
private let maxDisplayLines = 10
|
||||
|
||||
private var dateFormatter: DateFormatter {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
||||
return formatter
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(logs.suffix(maxDisplayLines), id: \.self) { log in
|
||||
Text(log)
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(4)
|
||||
.background(Color.black.opacity(0.7))
|
||||
.cornerRadius(4)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
startLogFileWatching()
|
||||
}
|
||||
.onChange(of: logs) { newLogs in
|
||||
print("Logs updated: \(newLogs.count) entries")
|
||||
}
|
||||
}
|
||||
|
||||
private func getLatestLogFile() -> URL? {
|
||||
let logsDirectory = URL.documentsDirectory.appendingPathComponent("Logs")
|
||||
let currentDate = Date()
|
||||
|
||||
do {
|
||||
try fileManager.createDirectory(at: logsDirectory, withIntermediateDirectories: true)
|
||||
|
||||
let logFiles = try fileManager.contentsOfDirectory(at: logsDirectory, includingPropertiesForKeys: [.creationDateKey])
|
||||
.filter {
|
||||
let filename = $0.lastPathComponent
|
||||
guard filename.hasPrefix("Ryujinx_ios_") && filename.hasSuffix(".log") else {
|
||||
return false
|
||||
}
|
||||
|
||||
let dateString = filename.replacingOccurrences(of: "Ryujinx_ios_", with: "").replacingOccurrences(of: ".log", with: "")
|
||||
guard let logDate = dateFormatter.date(from: dateString) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return Calendar.current.isDate(logDate, inSameDayAs: currentDate)
|
||||
}
|
||||
|
||||
let sortedLogFiles = logFiles.sorted {
|
||||
$0.lastPathComponent > $1.lastPathComponent
|
||||
}
|
||||
|
||||
return sortedLogFiles.first
|
||||
} catch {
|
||||
print("Error finding log files: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func readLatestLogFile() {
|
||||
guard let logFileURL = getLatestLogFile() else {
|
||||
print("no logs?")
|
||||
return
|
||||
}
|
||||
print(logFileURL)
|
||||
|
||||
do {
|
||||
let logContents = try String(contentsOf: logFileURL)
|
||||
let allLines = logContents.components(separatedBy: .newlines)
|
||||
|
||||
DispatchQueue.global(qos: .userInteractive).async {
|
||||
self.logs = Array(allLines)
|
||||
}
|
||||
} catch {
|
||||
print("Error reading log file: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func startLogFileWatching() {
|
||||
showingLogs = true
|
||||
self.readLatestLogFile()
|
||||
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
||||
if showingLogs {
|
||||
self.readLatestLogFile()
|
||||
}
|
||||
|
||||
if isfps {
|
||||
if get_current_fps() != 0 {
|
||||
stopLogFileWatching()
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopLogFileWatching() {
|
||||
showingLogs = false
|
||||
}
|
||||
}
|
@ -42,6 +42,12 @@ struct SettingsView: View {
|
||||
|
||||
@AppStorage("On-ScreenControllerScale") var controllerScale: Double = 1.0
|
||||
|
||||
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
|
||||
|
||||
@AppStorage("showlogsloading") var showlogsloading: Bool = true
|
||||
|
||||
@AppStorage("showlogsgame") var showlogsgame: Bool = false
|
||||
|
||||
@State private var showResolutionInfo = false
|
||||
@State private var showAnisotropicInfo = false
|
||||
@State private var showControllerInfo = false
|
||||
@ -275,7 +281,7 @@ struct SettingsView: View {
|
||||
|
||||
// Input Settings
|
||||
Section {
|
||||
Toggle(isOn: $config.macroHLE) {
|
||||
Toggle(isOn: $config.handHeldController) {
|
||||
labelWithIcon("Player 1 to Handheld Input", iconName: "formfitting.gamecontroller")
|
||||
}.tint(.blue)
|
||||
|
||||
@ -382,7 +388,7 @@ struct SettingsView: View {
|
||||
labelWithIcon("Disable PTC", iconName: "cpu")
|
||||
}.tint(.blue)
|
||||
|
||||
if let cpuInfo = getCPUInfo(), cpuInfo.hasPrefix("Apple M") {
|
||||
if let gpuInfo = getGPUInfo(), gpuInfo.hasPrefix("Apple M") {
|
||||
if #available (iOS 16.4, *) {
|
||||
Toggle(isOn: .constant(false)) {
|
||||
labelWithIcon("Hypervisor", iconName: "bolt")
|
||||
@ -390,7 +396,7 @@ struct SettingsView: View {
|
||||
.tint(.blue)
|
||||
.disabled(true)
|
||||
.onAppear() {
|
||||
print("CPU Info: \(cpuInfo)")
|
||||
print("CPU Info: \(gpuInfo)")
|
||||
}
|
||||
} else if checkAppEntitlement("com.apple.private.hypervisor") {
|
||||
Toggle(isOn: $config.hypervisor) {
|
||||
@ -398,7 +404,7 @@ struct SettingsView: View {
|
||||
}
|
||||
.tint(.blue)
|
||||
.onAppear() {
|
||||
print("CPU Info: \(cpuInfo)")
|
||||
print("CPU Info: \(gpuInfo)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -489,6 +495,14 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
DisclosureGroup {
|
||||
Toggle(isOn: $showlogsloading) {
|
||||
labelWithIcon("Show logs while loading", iconName: "text.alignleft")
|
||||
}.tint(.blue)
|
||||
|
||||
Toggle(isOn: $showlogsgame) {
|
||||
labelWithIcon("Show logs in-game", iconName: "text.line.magnify")
|
||||
}.tint(.blue)
|
||||
|
||||
Toggle(isOn: $config.debuglogs) {
|
||||
labelWithIcon("Debug Logs", iconName: "exclamationmark.bubble")
|
||||
}
|
||||
@ -529,7 +543,11 @@ struct SettingsView: View {
|
||||
|
||||
labelWithIcon("Device: \(getDeviceModel())", iconName: iconName)
|
||||
|
||||
labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / 1_000_000_000))", iconName: "memorychip.fill")
|
||||
if ProcessInfo.processInfo.isiOSAppOnMac {
|
||||
labelWithIcon("Device Memory: \(String(format: "%.0f GB", Double(totalMemory) / (1024 * 1024 * 1024)))", iconName: "memorychip.fill")
|
||||
} else {
|
||||
labelWithIcon("Device Memory: \(String(format: "%.0f GB", (Double(totalMemory) / (1024 * 1024 * 1024) + 1)))", iconName: "memorychip.fill")
|
||||
}
|
||||
|
||||
labelWithIcon("\(deviceType) \(UIDevice.current.systemVersion)", iconName: "applelogo")
|
||||
|
||||
@ -544,11 +562,6 @@ struct SettingsView: View {
|
||||
|
||||
// Advanced
|
||||
Section {
|
||||
Toggle(isOn: $windowCode) {
|
||||
labelWithIcon("SDL Window", iconName: "macwindow.on.rectangle")
|
||||
}
|
||||
.tint(.blue)
|
||||
|
||||
DisclosureGroup {
|
||||
|
||||
Toggle(isOn: $mVKPreFillBuffer) {
|
||||
@ -567,6 +580,10 @@ struct SettingsView: View {
|
||||
|
||||
}
|
||||
|
||||
Toggle(isOn: $ignoreJIT) {
|
||||
labelWithIcon("Ignore JIT Popup", iconName: "cpu")
|
||||
}.tint(.blue)
|
||||
|
||||
TextField("Additional Arguments", text: Binding(
|
||||
get: {
|
||||
config.additionalArgs.joined(separator: " ")
|
||||
@ -580,12 +597,13 @@ struct SettingsView: View {
|
||||
.textInputAutocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
|
||||
|
||||
|
||||
Button {
|
||||
Ryujinx.shared.removeFirmware()
|
||||
finishedStorage = false
|
||||
|
||||
} label: {
|
||||
Text("Remove Firmware")
|
||||
Text("Show Setup")
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
@ -599,11 +617,7 @@ struct SettingsView: View {
|
||||
.textCase(nil)
|
||||
.headerProminence(.increased)
|
||||
} footer: {
|
||||
if #available(iOS 17, *) {
|
||||
Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")")
|
||||
} else {
|
||||
Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). If the emulation is not showing (you may hear audio in some games), try enabling \"SDL Window\" \n \n\(gamepo ? "the cake is a lie" : "")")
|
||||
}
|
||||
Text("For advanced users. See page size or add custom arguments for experimental features. (Please don't touch this if you don't know what you're doing). \n \n\(gamepo ? "the cake is a lie" : "")")
|
||||
}
|
||||
|
||||
}
|
||||
@ -614,6 +628,8 @@ struct SettingsView: View {
|
||||
.onAppear {
|
||||
if let configs = loadSettings() {
|
||||
self.config = configs
|
||||
} else {
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
.onChange(of: config) { _ in
|
||||
@ -631,6 +647,10 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func saveSettings() {
|
||||
MeloNX.saveSettings(config: config)
|
||||
}
|
||||
|
||||
func getDeviceModel() -> String {
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
@ -641,26 +661,9 @@ struct SettingsView: View {
|
||||
}
|
||||
return identifier
|
||||
}
|
||||
|
||||
|
||||
func saveSettings() {
|
||||
#if targetEnvironment(simulator)
|
||||
|
||||
print("Saving Settings")
|
||||
#else
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
let data = try encoder.encode(config)
|
||||
let jsonString = String(data: data, encoding: .utf8)
|
||||
UserDefaults.standard.set(jsonString, forKey: "config")
|
||||
} catch {
|
||||
print("Failed to save settings: \(error)")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func getCPUInfo() -> String? {
|
||||
func getGPUInfo() -> String? {
|
||||
let device = MTLCreateSystemDefaultDevice()
|
||||
|
||||
let gpu = device?.name
|
||||
@ -669,29 +672,6 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
|
||||
// Original loadSettings function assumed to exist
|
||||
func loadSettings() -> Ryujinx.Configuration? {
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
print("Running on Simulator")
|
||||
|
||||
return Ryujinx.Configuration(gamepath: "")
|
||||
#else
|
||||
guard let jsonString = UserDefaults.standard.string(forKey: "config"),
|
||||
let data = jsonString.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
|
||||
return configs
|
||||
} catch {
|
||||
print("Failed to load settings: \(error)")
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func labelWithIcon(_ text: String, iconName: String, flipimage: Bool? = nil) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
@ -724,7 +704,7 @@ struct SVGView: UIViewRepresentable {
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
var svgName = svgName
|
||||
var hammock = UIView()
|
||||
let hammock = UIView()
|
||||
|
||||
if svgName.hasSuffix(".svg") {
|
||||
svgName.removeLast(4)
|
||||
@ -732,7 +712,7 @@ struct SVGView: UIViewRepresentable {
|
||||
|
||||
|
||||
|
||||
let svgLayer = UIView(SVGNamed: svgName) { svgLayer in
|
||||
_ = UIView(svgNamed: svgName) { svgLayer in
|
||||
svgLayer.fillColor = UIColor(color).cgColor // Apply the provided color
|
||||
svgLayer.resizeToFit(hammock.frame)
|
||||
hammock.layer.addSublayer(svgLayer)
|
||||
@ -748,3 +728,38 @@ struct SVGView: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveSettings(config: Ryujinx.Configuration) {
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
let data = try encoder.encode(config)
|
||||
|
||||
let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
|
||||
|
||||
try data.write(to: fileURL)
|
||||
print("Settings saved to: \(fileURL.path)")
|
||||
} catch {
|
||||
print("Failed to save settings: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func loadSettings() -> Ryujinx.Configuration? {
|
||||
do {
|
||||
let fileURL = URL.documentsDirectory.appendingPathComponent("config.json")
|
||||
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||
print("Config file does not exist at: \(fileURL.path)")
|
||||
return nil
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let configs = try decoder.decode(Ryujinx.Configuration.self, from: data)
|
||||
return configs
|
||||
} catch {
|
||||
print("Failed to load settings: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
@ -18,11 +18,26 @@ struct MeloNXApp: App {
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State var alert: UIAlertController? = nil
|
||||
|
||||
@State var finished = false
|
||||
@AppStorage("hasbeenfinished") var finishedStorage: Bool = false
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ZStack {
|
||||
if showed || DRM != 1 {
|
||||
ContentView()
|
||||
|
||||
if finishedStorage {
|
||||
ContentView()
|
||||
} else {
|
||||
SetupView(finished: $finished)
|
||||
.onChange(of: finished) { newValue in
|
||||
withAnimation {
|
||||
withAnimation {
|
||||
finishedStorage = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Group {
|
||||
VStack {
|
394
src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
Normal file
394
src/MeloNX/MeloNX/App/Views/Setup/SetupView.swift
Normal file
@ -0,0 +1,394 @@
|
||||
//
|
||||
// SetupView.swift
|
||||
// MeloNX
|
||||
//
|
||||
// Created by Stossy11 on 04/03/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct SetupView: View {
|
||||
@State private var isImportingKeys = false
|
||||
@State private var isImportingFirmware = false
|
||||
@State private var showAlert = false
|
||||
@State private var showSkipAlert = false
|
||||
@State private var alertMessage = ""
|
||||
@State private var keysImported = false
|
||||
@State private var firmImported = false
|
||||
@Binding var finished: Bool
|
||||
|
||||
var body: some View {
|
||||
iOSNav {
|
||||
ZStack {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
iPadSetupView(
|
||||
finished: $finished,
|
||||
isImportingKeys: $isImportingKeys,
|
||||
isImportingFirmware: $isImportingFirmware,
|
||||
keysImported: keysImported,
|
||||
firmImported: firmImported
|
||||
)
|
||||
} else {
|
||||
iPhoneSetupView(
|
||||
finished: $finished,
|
||||
isImportingKeys: $isImportingKeys,
|
||||
isImportingFirmware: $isImportingFirmware,
|
||||
keysImported: keysImported,
|
||||
firmImported: firmImported
|
||||
)
|
||||
}
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $isImportingKeys,
|
||||
allowedContentTypes: [.item],
|
||||
allowsMultipleSelection: true
|
||||
) { result in
|
||||
handleKeysImport(result: result)
|
||||
}
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $isImportingFirmware,
|
||||
allowedContentTypes: [.item],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
handleFirmwareImport(result: result)
|
||||
}
|
||||
.alert(alertMessage, isPresented: $showAlert) {
|
||||
Button("OK", role: .cancel) {}
|
||||
}
|
||||
.alert("Skip Setup?", isPresented: $showSkipAlert) {
|
||||
Button("Skip", role: .destructive) { finished = true }
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
.onAppear {
|
||||
initialize()
|
||||
finished = false
|
||||
keysImported = Ryujinx.shared.checkIfKeysImported()
|
||||
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
|
||||
}
|
||||
}
|
||||
|
||||
private func iPadSetupView(
|
||||
finished: Binding<Bool>,
|
||||
isImportingKeys: Binding<Bool>,
|
||||
isImportingFirmware: Binding<Bool>,
|
||||
keysImported: Bool,
|
||||
firmImported: Bool
|
||||
) -> some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color.blue.opacity(0.1),
|
||||
Color.red.opacity(0.1)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
|
||||
HStack(spacing: 40) {
|
||||
if geometry.size.width > 800 {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Image(uiImage: UIImage(named: appIcon()) ?? UIImage())
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 200, height: 200)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 40))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 40)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
.blue.opacity(0.6),
|
||||
.red.opacity(0.6)
|
||||
]),
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
),
|
||||
lineWidth: 2
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.1), radius: 15, x: 0, y: 6)
|
||||
|
||||
Text("Welcome to MeloNX")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("Set up your Nintendo Switch emulation environment by importing keys and firmware.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding()
|
||||
}
|
||||
.frame(maxWidth: 400)
|
||||
}
|
||||
|
||||
VStack(spacing: 20) {
|
||||
setupStep(
|
||||
title: "Import Keys",
|
||||
description: "Add your encryption keys",
|
||||
systemImage: "key.fill",
|
||||
isCompleted: keysImported,
|
||||
action: { isImportingKeys.wrappedValue = true }
|
||||
)
|
||||
|
||||
setupStep(
|
||||
title: "Add Firmware",
|
||||
description: "Install Nintendo Switch firmware",
|
||||
systemImage: "square.and.arrow.down",
|
||||
isCompleted: firmImported,
|
||||
isEnabled: keysImported,
|
||||
action: { isImportingFirmware.wrappedValue = true }
|
||||
)
|
||||
|
||||
Button(action: { finished.wrappedValue = true }) {
|
||||
HStack {
|
||||
Text("Finish Setup")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(
|
||||
firmImported && keysImported
|
||||
? Color.blue
|
||||
: Color.blue.opacity(0.3)
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.disabled(!(firmImported && keysImported))
|
||||
}
|
||||
.frame(maxWidth: 500)
|
||||
.padding()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Setup")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
private func iPhoneSetupView(
|
||||
finished: Binding<Bool>,
|
||||
isImportingKeys: Binding<Bool>,
|
||||
isImportingFirmware: Binding<Bool>,
|
||||
keysImported: Bool,
|
||||
firmImported: Bool
|
||||
) -> some View {
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
Color.blue.opacity(0.1),
|
||||
Color.red.opacity(0.1)
|
||||
]),
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
Image(uiImage: UIImage(named: appIcon()) ?? UIImage())
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 120, height: 120)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.stroke(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [
|
||||
.blue.opacity(0.6),
|
||||
.red.opacity(0.6)
|
||||
]),
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
),
|
||||
lineWidth: 2
|
||||
)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 4)
|
||||
.padding(.top, 40)
|
||||
|
||||
Text("Welcome to MeloNX")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
.padding(.bottom, 20)
|
||||
.onTapGesture(count: 2) {
|
||||
showSkipAlert = true
|
||||
}
|
||||
|
||||
setupStep(
|
||||
title: "Import Keys",
|
||||
description: "Add your encryption keys",
|
||||
systemImage: "key.fill",
|
||||
isCompleted: keysImported,
|
||||
action: { isImportingKeys.wrappedValue = true }
|
||||
)
|
||||
|
||||
setupStep(
|
||||
title: "Add Firmware",
|
||||
description: "Install Nintendo Switch firmware",
|
||||
systemImage: "square.and.arrow.down",
|
||||
isCompleted: firmImported,
|
||||
isEnabled: keysImported,
|
||||
action: { isImportingFirmware.wrappedValue = true }
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
// Finish Button
|
||||
VStack {
|
||||
Button(action: { finished.wrappedValue = true }) {
|
||||
HStack {
|
||||
Text("Finish Setup")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(
|
||||
firmImported && keysImported
|
||||
? Color.blue
|
||||
: Color.blue.opacity(0.3)
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.disabled(!(firmImported && keysImported))
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Setup")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupStep(
|
||||
title: String,
|
||||
description: String,
|
||||
systemImage: String,
|
||||
isCompleted: Bool,
|
||||
isEnabled: Bool = true,
|
||||
action: @escaping () -> Void
|
||||
) -> some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Image(systemName: systemImage)
|
||||
.foregroundColor(isCompleted ? .green : .blue)
|
||||
.imageScale(.large)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
Text(description)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if isCompleted {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(UIColor.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.disabled(!isEnabled || isCompleted)
|
||||
.opacity(isEnabled ? 1.0 : 0.5)
|
||||
}
|
||||
|
||||
private func handleKeysImport(result: Result<[URL], Error>) {
|
||||
do {
|
||||
let selectedFiles = try result.get()
|
||||
|
||||
guard selectedFiles.count == 2 else {
|
||||
alertMessage = "Please select exactly 2 key files"
|
||||
showAlert = true
|
||||
return
|
||||
}
|
||||
|
||||
for fileURL in selectedFiles {
|
||||
guard fileURL.startAccessingSecurityScopedResource() else {
|
||||
alertMessage = "Permission denied to access file"
|
||||
showAlert = true
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
fileURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
let destinationURL = URL.documentsDirectory.appendingPathComponent("system").appendingPathComponent(fileURL.lastPathComponent)
|
||||
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL)
|
||||
}
|
||||
|
||||
keysImported = Ryujinx.shared.checkIfKeysImported()
|
||||
alertMessage = "Keys imported successfully"
|
||||
showAlert = true
|
||||
|
||||
} catch {
|
||||
alertMessage = "Error importing keys: \(error.localizedDescription)"
|
||||
showAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
private func handleFirmwareImport(result: Result<[URL], Error>) {
|
||||
do {
|
||||
let selectedFiles = try result.get()
|
||||
|
||||
guard let fileURL = selectedFiles.first else {
|
||||
alertMessage = "No file selected"
|
||||
showAlert = true
|
||||
return
|
||||
}
|
||||
|
||||
guard fileURL.startAccessingSecurityScopedResource() else {
|
||||
alertMessage = "Permission denied to access file"
|
||||
showAlert = true
|
||||
return
|
||||
}
|
||||
|
||||
defer {
|
||||
fileURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
||||
Ryujinx.shared.installFirmware(firmwarePath: fileURL.path)
|
||||
|
||||
|
||||
firmImported = (Ryujinx.shared.fetchFirmwareVersion() != "0")
|
||||
alertMessage = "Firmware installed successfully"
|
||||
showAlert = true
|
||||
|
||||
} catch {
|
||||
alertMessage = "Error importing firmware: \(error.localizedDescription)"
|
||||
showAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
func appIcon(in bundle: Bundle = .main) -> String {
|
||||
guard let icons = bundle.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
|
||||
|
||||
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
|
||||
|
||||
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
|
||||
|
||||
let iconFileName = iconFiles.last else {
|
||||
|
||||
print("Could not find icons in bundle")
|
||||
return ""
|
||||
}
|
||||
|
||||
return iconFileName
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
private static readonly string[] _desirableExtensions = {
|
||||
ExtConditionalRendering.ExtensionName,
|
||||
ExtExtendedDynamicState.ExtensionName,
|
||||
ExtExtendedDynamicState.ExtensionName, // This is unsupported on iOS 16 and below.
|
||||
ExtTransformFeedback.ExtensionName,
|
||||
KhrDrawIndirectCount.ExtensionName,
|
||||
KhrPushDescriptor.ExtensionName,
|
||||
|
@ -0,0 +1,81 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Ipc;
|
||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||
using Ryujinx.Horizon.Common;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.BluetoothManager.BtmSystem
|
||||
{
|
||||
class IBtmSystemCore : IpcService
|
||||
{
|
||||
public KEvent _radioEvent;
|
||||
public int _radioEventhandle;
|
||||
|
||||
public KEvent _gamepadPairingEvent;
|
||||
public int _gamepadPairingEventHandle;
|
||||
|
||||
public IBtmSystemCore() { }
|
||||
|
||||
[CommandCmif(6)]
|
||||
// IsRadioEnabled() -> b8
|
||||
public ResultCode IsRadioEnabled(ServiceCtx context)
|
||||
{
|
||||
context.ResponseData.Write(true);
|
||||
|
||||
Logger.Stub?.PrintStub(LogClass.ServiceBtm);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(7)] // 3.0.0+
|
||||
// AcquireRadioEvent() -> (byte<1>, handle<copy>)
|
||||
public ResultCode AcquireRadioEvent(ServiceCtx context)
|
||||
{
|
||||
Result result = Result.Success;
|
||||
|
||||
if (_radioEventhandle == 0)
|
||||
{
|
||||
_radioEvent = new KEvent(context.Device.System.KernelContext);
|
||||
|
||||
result = context.Process.HandleTable.GenerateHandle(_radioEvent.ReadableEvent, out _radioEventhandle);
|
||||
|
||||
if (result != Result.Success)
|
||||
{
|
||||
// NOTE: We use a Logging instead of an exception because the call return a boolean if succeed or not.
|
||||
Logger.Error?.Print(LogClass.ServiceBsd, "Out of handles!");
|
||||
}
|
||||
}
|
||||
|
||||
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_radioEventhandle);
|
||||
|
||||
context.ResponseData.Write(result == Result.Success ? 1 : 0);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(8)] // 3.0.0+
|
||||
// AcquireGamepadPairingEvent() -> (byte<1>, handle<copy>)
|
||||
public ResultCode AcquireGamepadPairingEvent(ServiceCtx context)
|
||||
{
|
||||
Result result = Result.Success;
|
||||
|
||||
if (_gamepadPairingEventHandle == 0)
|
||||
{
|
||||
_gamepadPairingEvent = new KEvent(context.Device.System.KernelContext);
|
||||
|
||||
result = context.Process.HandleTable.GenerateHandle(_gamepadPairingEvent.ReadableEvent, out _gamepadPairingEventHandle);
|
||||
|
||||
if (result != Result.Success)
|
||||
{
|
||||
// NOTE: We use a Logging instead of an exception because the call return a boolean if succeed or not.
|
||||
Logger.Error?.Print(LogClass.ServiceBsd, "Out of handles!");
|
||||
}
|
||||
}
|
||||
|
||||
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_gamepadPairingEventHandle);
|
||||
|
||||
context.ResponseData.Write(result == Result.Success ? 1 : 0);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
}
|
||||
}
|
@ -122,7 +122,8 @@ namespace Ryujinx.HLE.HOS.Services
|
||||
if (serviceExists)
|
||||
{
|
||||
Logger.Trace?.Print(LogClass.KernelIpc, $"{service.GetType().Name}: {processRequest.Name}");
|
||||
|
||||
|
||||
|
||||
result = (ResultCode)processRequest.Invoke(service, new object[] { context });
|
||||
}
|
||||
else
|
||||
|
173
src/Ryujinx.Headless.SDL2/MoltenVK/MoltenVKWindow.cs
Normal file
173
src/Ryujinx.Headless.SDL2/MoltenVK/MoltenVKWindow.cs
Normal file
@ -0,0 +1,173 @@
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Input.HLE;
|
||||
using Ryujinx.SDL2.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using static SDL2.SDL;
|
||||
using Silk.NET.Vulkan;
|
||||
using Silk.NET.Vulkan.Extensions.EXT;
|
||||
using Silk.NET.Vulkan.Extensions.KHR;
|
||||
|
||||
namespace Ryujinx.Headless.SDL2.Vulkan
|
||||
{
|
||||
class MoltenVKWindow : WindowBase
|
||||
{
|
||||
public IntPtr nativeMetalLayer = IntPtr.Zero;
|
||||
|
||||
private Vk _vk;
|
||||
private ExtMetalSurface _metalSurface;
|
||||
private SurfaceKHR _surface;
|
||||
private bool _surfaceCreated;
|
||||
|
||||
public MoltenVKWindow(
|
||||
InputManager inputManager,
|
||||
GraphicsDebugLevel glLogLevel,
|
||||
AspectRatio aspectRatio,
|
||||
bool enableMouse,
|
||||
HideCursorMode hideCursorMode) : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
|
||||
{
|
||||
_vk = Vk.GetApi();
|
||||
_surfaceCreated = false;
|
||||
}
|
||||
|
||||
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_VULKAN;
|
||||
|
||||
protected override void InitializeWindowRenderer() {}
|
||||
|
||||
protected override void InitializeRenderer()
|
||||
{
|
||||
if (IsExclusiveFullscreen)
|
||||
{
|
||||
Renderer?.Window.SetSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
|
||||
MouseDriver.SetClientSize(ExclusiveFullscreenWidth, ExclusiveFullscreenHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
Renderer?.Window.SetSize(DefaultWidth, DefaultHeight);
|
||||
MouseDriver.SetClientSize(DefaultWidth, DefaultHeight);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetNativeWindow(IntPtr metalLayer)
|
||||
{
|
||||
if (metalLayer == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
nativeMetalLayer = IntPtr.Zero;
|
||||
nativeMetalLayer = metalLayer;
|
||||
}
|
||||
|
||||
private static void BasicInvoke(Action action)
|
||||
{
|
||||
action();
|
||||
}
|
||||
|
||||
public unsafe IntPtr CreateWindowSurface(IntPtr instanceHandle)
|
||||
{
|
||||
if (_surfaceCreated)
|
||||
{
|
||||
return (IntPtr)(ulong)_surface.Handle;
|
||||
}
|
||||
|
||||
if (nativeMetalLayer == IntPtr.Zero)
|
||||
{
|
||||
throw new Exception("Cannot create Vulkan surface: No CAMetalLayer set");
|
||||
}
|
||||
|
||||
var instance = new Instance((nint)instanceHandle);
|
||||
if (!_vk.TryGetInstanceExtension(instance, out _metalSurface))
|
||||
{
|
||||
throw new Exception("Failed to get ExtMetalSurface extension");
|
||||
}
|
||||
|
||||
var createInfo = new MetalSurfaceCreateInfoEXT
|
||||
{
|
||||
SType = StructureType.MetalSurfaceCreateInfoExt,
|
||||
PNext = null,
|
||||
PLayer = (nint*)nativeMetalLayer
|
||||
};
|
||||
|
||||
SurfaceKHR* surfacePtr = stackalloc SurfaceKHR[1];
|
||||
Result result = _metalSurface.CreateMetalSurface(instance, &createInfo, null, surfacePtr);
|
||||
if (result != Result.Success)
|
||||
{
|
||||
throw new Exception($"vkCreateMetalSurfaceEXT failed with error code {result}");
|
||||
}
|
||||
|
||||
_surface = *surfacePtr;
|
||||
_surfaceCreated = true;
|
||||
|
||||
return (IntPtr)(ulong)_surface.Handle;
|
||||
}
|
||||
|
||||
public unsafe string[] GetRequiredInstanceExtensions()
|
||||
{
|
||||
List<string> requiredExtensions = new List<string>
|
||||
{
|
||||
"VK_KHR_surface",
|
||||
"VK_EXT_metal_surface"
|
||||
};
|
||||
|
||||
uint extensionCount = 0;
|
||||
_vk.EnumerateInstanceExtensionProperties((byte*)null, &extensionCount, null);
|
||||
|
||||
if (extensionCount == 0)
|
||||
{
|
||||
string errorMessage = "Failed to enumerate Vulkan instance extensions";
|
||||
Logger.Error?.Print(LogClass.Application, errorMessage);
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
ExtensionProperties* extensions = stackalloc ExtensionProperties[(int)extensionCount];
|
||||
|
||||
Result result = _vk.EnumerateInstanceExtensionProperties((byte*)null, &extensionCount, extensions);
|
||||
|
||||
if (result != Result.Success)
|
||||
{
|
||||
string errorMessage = $"Failed to enumerate Vulkan instance extensions, error: {result}";
|
||||
Logger.Error?.Print(LogClass.Application, errorMessage);
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
List<string> availableExtensions = new List<string>();
|
||||
|
||||
for (int i = 0; i < extensionCount; i++)
|
||||
{
|
||||
string extName = Marshal.PtrToStringAnsi((IntPtr)extensions[i].ExtensionName);
|
||||
availableExtensions.Add(extName);
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"Available Vulkan extensions: {string.Join(", ", availableExtensions)}");
|
||||
|
||||
foreach (string requiredExt in requiredExtensions)
|
||||
{
|
||||
if (!availableExtensions.Contains(requiredExt))
|
||||
{
|
||||
string errorMessage = $"Required Vulkan extension {requiredExt} is not available";
|
||||
Logger.Error?.Print(LogClass.Application, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info?.Print(LogClass.Application, $"Using Vulkan extensions: {string.Join(", ", requiredExtensions)}");
|
||||
|
||||
return requiredExtensions.ToArray();
|
||||
}
|
||||
|
||||
protected override void FinalizeWindowRenderer()
|
||||
{
|
||||
if (_surfaceCreated)
|
||||
{
|
||||
_surface = default;
|
||||
_surfaceCreated = false;
|
||||
}
|
||||
|
||||
nativeMetalLayer = IntPtr.Zero;
|
||||
}
|
||||
|
||||
|
||||
protected override void SwapBuffers() {}
|
||||
}
|
||||
}
|
@ -113,6 +113,8 @@ namespace Ryujinx.Headless.SDL2
|
||||
private static List<InputConfig> _inputConfiguration;
|
||||
private static bool _enableKeyboard;
|
||||
private static bool _enableMouse;
|
||||
private static IntPtr nativeMetalLayer = IntPtr.Zero;
|
||||
private static readonly object metalLayerLock = new object();
|
||||
|
||||
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
@ -142,6 +144,19 @@ namespace Ryujinx.Headless.SDL2
|
||||
return 0;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "set_native_window")]
|
||||
public static unsafe void SetNativeWindow(IntPtr layer) {
|
||||
lock (metalLayerLock) {
|
||||
nativeMetalLayer = layer;
|
||||
Logger.Info?.Print(LogClass.Application, $"SetNativeWindow called with layer: {layer}");
|
||||
}
|
||||
}
|
||||
|
||||
public static IntPtr GetNativeMetalLayer()
|
||||
{
|
||||
return nativeMetalLayer;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "get_dlc_nca_list")]
|
||||
public static unsafe DlcNcaList GetDlcNcaList(IntPtr titleIdPtr, IntPtr pathPtr)
|
||||
{
|
||||
@ -391,12 +406,13 @@ namespace Ryujinx.Headless.SDL2
|
||||
[UnmanagedCallersOnly(EntryPoint = "stop_emulation")]
|
||||
public static void StopEmulation()
|
||||
{
|
||||
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Exit();
|
||||
_emulationContext.Dispose();
|
||||
_emulationContext = null;
|
||||
if (_window._isPaused) {
|
||||
_window._isPaused = false;
|
||||
} else {
|
||||
_window._isPaused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -977,8 +993,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
}
|
||||
else
|
||||
{
|
||||
bool isAppleController = gamepadName.Contains("Apple") ? option.OnScreenCorrespond : false;
|
||||
bool isNintendoStyle = gamepadName.Contains("Nintendo") || isAppleController;
|
||||
bool isNintendoStyle = gamepadName.Contains("Nintendo") || gamepadName.Contains("Joycons");
|
||||
|
||||
config = new StandardControllerInputConfig
|
||||
{
|
||||
@ -1190,7 +1205,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
}
|
||||
|
||||
if (option.InputPath == "MiiMaker") {
|
||||
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001000, StorageId.BuiltInSystem, NcaContentType.Program);
|
||||
|
||||
option.InputPath = contentPath;
|
||||
}
|
||||
@ -1292,18 +1307,25 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
private static WindowBase CreateWindow(Options options)
|
||||
{
|
||||
return options.GraphicsBackend == GraphicsBackend.Vulkan
|
||||
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode)
|
||||
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
|
||||
if (OperatingSystem.IsIOS()) {
|
||||
return new MoltenVKWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
return options.GraphicsBackend == GraphicsBackend.Vulkan
|
||||
? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode)
|
||||
: new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
|
||||
}
|
||||
}
|
||||
|
||||
private static IRenderer CreateRenderer(Options options, WindowBase window)
|
||||
{
|
||||
if (options.GraphicsBackend == GraphicsBackend.Vulkan && window is VulkanWindow vulkanWindow)
|
||||
if (options.GraphicsBackend == GraphicsBackend.Vulkan)
|
||||
{
|
||||
string preferredGpuId = string.Empty;
|
||||
Vk api = Vk.GetApi();
|
||||
|
||||
// Handle GPU preference selection
|
||||
if (!string.IsNullOrEmpty(options.PreferredGPUVendor))
|
||||
{
|
||||
string preferredGpuVendor = options.PreferredGPUVendor.ToLowerInvariant();
|
||||
@ -1319,13 +1341,25 @@ namespace Ryujinx.Headless.SDL2
|
||||
}
|
||||
}
|
||||
|
||||
return new VulkanRenderer(
|
||||
api,
|
||||
(instance, vk) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))),
|
||||
vulkanWindow.GetRequiredInstanceExtensions,
|
||||
preferredGpuId);
|
||||
if (window is VulkanWindow vulkanWindow)
|
||||
{
|
||||
return new VulkanRenderer(
|
||||
api,
|
||||
(instance, vk) => new SurfaceKHR((ulong)(vulkanWindow.CreateWindowSurface(instance.Handle))),
|
||||
vulkanWindow.GetRequiredInstanceExtensions,
|
||||
preferredGpuId);
|
||||
}
|
||||
else if (window is MoltenVKWindow mvulkanWindow)
|
||||
{
|
||||
return new VulkanRenderer(
|
||||
api,
|
||||
(instance, vk) => new SurfaceKHR((ulong)(mvulkanWindow.CreateWindowSurface(instance.Handle))),
|
||||
mvulkanWindow.GetRequiredInstanceExtensions,
|
||||
preferredGpuId);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to OpenGL renderer if Vulkan is not used
|
||||
return new OpenGLRenderer();
|
||||
}
|
||||
|
||||
@ -1333,12 +1367,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
BackendThreading threadingMode = options.BackendThreading;
|
||||
|
||||
bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
|
||||
|
||||
if (threadedGAL)
|
||||
{
|
||||
renderer = new ThreadedRenderer(renderer);
|
||||
}
|
||||
renderer = new ThreadedRenderer(renderer);
|
||||
|
||||
bool AppleHV = false;
|
||||
|
||||
@ -1413,6 +1442,11 @@ namespace Ryujinx.Headless.SDL2
|
||||
Logger.RestartTime();
|
||||
|
||||
WindowBase window = CreateWindow(options);
|
||||
|
||||
if (window is MoltenVKWindow mvulkanWindow) {
|
||||
mvulkanWindow.SetNativeWindow(nativeMetalLayer);
|
||||
}
|
||||
|
||||
IRenderer renderer = CreateRenderer(options, window);
|
||||
|
||||
_window = window;
|
||||
|
@ -66,7 +66,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
public ScalingFilter ScalingFilter { get; set; }
|
||||
public int ScalingFilterLevel { get; set; }
|
||||
|
||||
public SDL2MouseDriver MouseDriver;
|
||||
public iOSTouchDriver MouseDriver;
|
||||
private readonly InputManager _inputManager;
|
||||
private readonly IKeyboard _keyboardInterface;
|
||||
private readonly GraphicsDebugLevel _glLogLevel;
|
||||
@ -81,6 +81,8 @@ namespace Ryujinx.Headless.SDL2
|
||||
private bool _isStopped;
|
||||
private uint _windowId;
|
||||
|
||||
public bool _isPaused = false;
|
||||
|
||||
private string _gpuVendorName;
|
||||
|
||||
private readonly AspectRatio _aspectRatio;
|
||||
@ -93,7 +95,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
bool enableMouse,
|
||||
HideCursorMode hideCursorMode)
|
||||
{
|
||||
MouseDriver = new SDL2MouseDriver(hideCursorMode);
|
||||
MouseDriver = new iOSTouchDriver(hideCursorMode);
|
||||
_inputManager = inputManager;
|
||||
_inputManager.SetMouseDriver(MouseDriver);
|
||||
NpadManager = _inputManager.CreateNpadManager();
|
||||
@ -159,48 +161,59 @@ namespace Ryujinx.Headless.SDL2
|
||||
|
||||
private void InitializeWindow()
|
||||
{
|
||||
var activeProcess = Device.Processes.ActiveApplication;
|
||||
var nacp = activeProcess.ApplicationControlProperties;
|
||||
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
|
||||
if (this is Ryujinx.Headless.SDL2.Vulkan.MoltenVKWindow) {
|
||||
string message = $"Not using SDL Windows, Skipping...";
|
||||
|
||||
string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
|
||||
string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
|
||||
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
|
||||
string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
|
||||
Logger.Info?.Print(LogClass.Application, message);
|
||||
|
||||
Width = DefaultWidth;
|
||||
Height = DefaultHeight;
|
||||
Width = DefaultWidth;
|
||||
Height = DefaultHeight;
|
||||
|
||||
if (IsExclusiveFullscreen)
|
||||
{
|
||||
Width = ExclusiveFullscreenWidth;
|
||||
Height = ExclusiveFullscreenHeight;
|
||||
MouseDriver.SetClientSize(Width, Height);
|
||||
} else {
|
||||
var activeProcess = Device.Processes.ActiveApplication;
|
||||
var nacp = activeProcess.ApplicationControlProperties;
|
||||
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
|
||||
|
||||
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
|
||||
string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
|
||||
string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
|
||||
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
|
||||
string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
|
||||
|
||||
Width = DefaultWidth;
|
||||
Height = DefaultHeight;
|
||||
|
||||
if (IsExclusiveFullscreen)
|
||||
{
|
||||
Width = ExclusiveFullscreenWidth;
|
||||
Height = ExclusiveFullscreenHeight;
|
||||
|
||||
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
else if (IsFullscreen)
|
||||
{
|
||||
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
|
||||
// WindowHandle = SDL_GetWindowFromID(1);
|
||||
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", 0, 0, Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
|
||||
|
||||
if (WindowHandle == IntPtr.Zero)
|
||||
{
|
||||
string errorMessage = $"SDL_CreateWindow failed with error \"{SDL_GetError()}\"";
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, errorMessage);
|
||||
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
SetWindowIcon();
|
||||
|
||||
_windowId = SDL_GetWindowID(WindowHandle);
|
||||
SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
|
||||
}
|
||||
else if (IsFullscreen)
|
||||
{
|
||||
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
|
||||
// WindowHandle = SDL_GetWindowFromID(1);
|
||||
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", 0, 0, Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
|
||||
|
||||
if (WindowHandle == IntPtr.Zero)
|
||||
{
|
||||
string errorMessage = $"SDL_CreateWindow failed with error \"{SDL_GetError()}\"";
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, errorMessage);
|
||||
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
|
||||
SetWindowIcon();
|
||||
|
||||
_windowId = SDL_GetWindowID(WindowHandle);
|
||||
SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
|
||||
}
|
||||
|
||||
private void HandleWindowEvent(SDL_Event evnt)
|
||||
@ -232,7 +245,7 @@ namespace Ryujinx.Headless.SDL2
|
||||
}
|
||||
else
|
||||
{
|
||||
MouseDriver.Update(evnt);
|
||||
// MouseDriver.Update(evnt);
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,6 +302,11 @@ namespace Ryujinx.Headless.SDL2
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ticks += _chrono.ElapsedTicks;
|
||||
|
||||
_chrono.Restart();
|
||||
@ -369,6 +387,13 @@ namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
while (_isActive)
|
||||
{
|
||||
|
||||
if (_isPaused)
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateFrame();
|
||||
|
||||
SDL_PumpEvents();
|
||||
@ -408,6 +433,11 @@ namespace Ryujinx.Headless.SDL2
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_isPaused)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_isStopped)
|
||||
{
|
||||
return false;
|
||||
@ -416,13 +446,12 @@ namespace Ryujinx.Headless.SDL2
|
||||
NpadManager.Update();
|
||||
|
||||
// Touchscreen
|
||||
bool hasTouch = false;
|
||||
bool hasTouch = true;
|
||||
|
||||
MouseDriver.SetClientSize(Width, Height);
|
||||
|
||||
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as iOSTouchDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
|
||||
|
||||
// Get screen touch position
|
||||
if (!_enableMouse)
|
||||
{
|
||||
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL2MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
|
||||
}
|
||||
|
||||
if (!hasTouch)
|
||||
{
|
||||
@ -545,11 +574,14 @@ namespace Ryujinx.Headless.SDL2
|
||||
TouchScreenManager?.Dispose();
|
||||
NpadManager.Dispose();
|
||||
|
||||
SDL2Driver.Instance.UnregisterWindow(_windowId);
|
||||
if (!(this is Ryujinx.Headless.SDL2.Vulkan.MoltenVKWindow))
|
||||
{
|
||||
SDL2Driver.Instance.UnregisterWindow(_windowId);
|
||||
|
||||
SDL_DestroyWindow(WindowHandle);
|
||||
SDL_DestroyWindow(WindowHandle);
|
||||
|
||||
SDL2Driver.Instance.Dispose();
|
||||
SDL2Driver.Instance.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
90
src/Ryujinx.Headless.SDL2/iOSMouse.cs
Normal file
90
src/Ryujinx.Headless.SDL2/iOSMouse.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
class iOSMouse : IMouse
|
||||
{
|
||||
private iOSTouchDriver _driver;
|
||||
|
||||
public GamepadFeaturesFlag Features => throw new NotImplementedException();
|
||||
|
||||
public string Id => "0";
|
||||
|
||||
public string Name => "iOSMouse";
|
||||
|
||||
public bool IsConnected => true;
|
||||
|
||||
public bool[] Buttons => _driver.PressedButtons;
|
||||
|
||||
Size IMouse.ClientSize => _driver.GetClientSize();
|
||||
|
||||
public iOSMouse(iOSTouchDriver driver)
|
||||
{
|
||||
_driver = driver;
|
||||
}
|
||||
|
||||
public Vector2 GetPosition()
|
||||
{
|
||||
return _driver.CurrentPosition;
|
||||
}
|
||||
|
||||
public Vector2 GetScroll()
|
||||
{
|
||||
return _driver.Scroll;
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetStateSnapshot()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public (float, float) GetStick(StickInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool IsButtonPressed(MouseButton button)
|
||||
{
|
||||
return _driver.IsButtonPressed(button);
|
||||
}
|
||||
|
||||
public bool IsPressed(GamepadButtonInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetConfiguration(InputConfig configuration)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_driver = null;
|
||||
}
|
||||
}
|
||||
}
|
156
src/Ryujinx.Headless.SDL2/iOSTouchDriver.cs
Normal file
156
src/Ryujinx.Headless.SDL2/iOSTouchDriver.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Headless.SDL2
|
||||
{
|
||||
class iOSTouchDriver : IGamepadDriver
|
||||
{
|
||||
private const int CursorHideIdleTime = 5;
|
||||
|
||||
private bool _isDisposed;
|
||||
private readonly HideCursorMode _hideCursorMode;
|
||||
private bool _isHidden;
|
||||
private long _lastCursorMoveTime;
|
||||
|
||||
public bool[] PressedButtons { get; }
|
||||
public Vector2 CurrentPosition { get; private set; }
|
||||
public Vector2 Scroll { get; private set; }
|
||||
public Size ClientSize;
|
||||
|
||||
private static Dictionary<int, Vector2> _activeTouches = new();
|
||||
|
||||
public iOSTouchDriver(HideCursorMode hideCursorMode)
|
||||
{
|
||||
PressedButtons = new bool[(int)MouseButton.Count];
|
||||
_hideCursorMode = hideCursorMode;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "touch_began")]
|
||||
public static void TouchBeganAtPoint(float x, float y, int index)
|
||||
{
|
||||
Vector2 position = new Vector2(x, y);
|
||||
_activeTouches[index] = position;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "touch_moved")]
|
||||
public static void TouchMovedAtPoint(float x, float y, int index)
|
||||
{
|
||||
if (_activeTouches.ContainsKey(index))
|
||||
{
|
||||
_activeTouches[index] = new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "touch_ended")]
|
||||
public static void TouchEndedForIndex(int index)
|
||||
{
|
||||
if (_activeTouches.ContainsKey(index))
|
||||
{
|
||||
_activeTouches.Remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePosition()
|
||||
{
|
||||
if (_activeTouches.Count > 0)
|
||||
{
|
||||
var touch = _activeTouches.Values.GetEnumerator();
|
||||
touch.MoveNext();
|
||||
Vector2 position = touch.Current;
|
||||
|
||||
if (CurrentPosition != position)
|
||||
{
|
||||
CurrentPosition = position;
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
CheckIdle();
|
||||
}
|
||||
|
||||
private void CheckIdle()
|
||||
{
|
||||
if (_hideCursorMode != HideCursorMode.OnIdle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
|
||||
|
||||
if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency)
|
||||
{
|
||||
if (!_isHidden)
|
||||
{
|
||||
Logger.Debug?.Print(LogClass.Application, "Hiding cursor due to inactivity.");
|
||||
_isHidden = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_isHidden)
|
||||
{
|
||||
Logger.Debug?.Print(LogClass.Application, "Showing cursor after activity.");
|
||||
_isHidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void SetClientSize(int width, int height)
|
||||
{
|
||||
ClientSize = new Size(width, height);
|
||||
}
|
||||
|
||||
public bool IsButtonPressed(MouseButton button)
|
||||
{
|
||||
if (_activeTouches.Count > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Size GetClientSize()
|
||||
{
|
||||
return ClientSize;
|
||||
}
|
||||
|
||||
public string DriverName => "iOSTouchDriver";
|
||||
|
||||
public event Action<string> OnGamepadConnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action<string> OnGamepadDisconnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds => new[] { "0" };
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
return new iOSMouse(this);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user