MeloNX/MeloNX-XC/MeloNX/Core/Ryujinx.swift
2024-11-01 15:56:05 +11:00

356 lines
11 KiB
Swift

//
// Ryujinx.swift
// MeloNX
//
// Created by Stossy11 on 27/10/2024.
//
import Foundation
import SwiftUI
enum RyujinxError: Error {
case libraryLoadError
case executionError(code: Int32)
case alreadyRunning
case notRunning
}
class RyujinxEmulator {
private var isRunning = false
private var emulationThread: Thread?
struct Configuration {
let inputPath: String
let mainThread: Bool // i don't know why i added this
let graphicsBackend: String
var additionalArgs: [String]
init(
inputPath: String,
mainThread: Bool = true,
graphicsBackend: String = "Vulkan",
additionalArgs: [String] = []
) {
self.inputPath = inputPath
self.mainThread = mainThread
self.graphicsBackend = graphicsBackend
self.additionalArgs = additionalArgs
}
}
private static func start(with config: Configuration) throws {
var args: [String] = []
// Taken from the POC
/*
var args: [String] = [
"--enable-debug-logs", "false", "--enable-trace-logs", "false", "--memory-manager-mode",
"SoftwarePageTable",
"--graphics-backend",
"Vulkan",
//"--enable-fs-integrity-checks", "false",
"--input-id-1", "0",
// "--list-inputs-ids", "true",
config.inputPath,
]
*/
args.append(config.inputPath)
args.append("--graphics-backend")
args.append(config.graphicsBackend)
// args.append(contentsOf: ["--memory-manager-mode", "SoftwarePageTable"])
// args.append(contentsOf: ["--fullscreen", "true"])
args.append(contentsOf: ["--enable-debug-logs", "true"])
args.append(contentsOf: ["--enable-trace-logs", "true"])
// args.append(contentsOf: ["--list-inputs-ids", "true"])
args.append(contentsOf: ["--input-id-1", "1-47150005-05ac-0000-0100-00004f066d01"])
// args.append("--input-path")
args.append(contentsOf: config.additionalArgs)
let cArgs = args.map { strdup($0) }
defer {
cArgs.forEach { ptr in
if let ptr = ptr {
free(ptr)
}
}
}
var argvPtrs = cArgs
let result = main_ryujinx_sdl(Int32(args.count), &argvPtrs)
if result != 0 {
throw RyujinxError.executionError(code: result)
}
}
// cray z
func startWithRunLoop(config: Configuration) throws {
guard !isRunning else {
throw RyujinxError.alreadyRunning
}
isRunning = true
emulationThread = Thread {
let runLoop = RunLoop.current
let port = Port()
runLoop.add(port, forMode: .default)
print(config.mainThread ? "Running on the main thread" : "Running on the background thread")
if config.mainThread {
DispatchQueue.main.async {
do {
try Self.start(with: config)
} catch {
Self.log("Emulation failed to start: \(error)")
self.isRunning = false
return
}
}
} else {
do {
try Self.start(with: config)
} catch {
Self.log("Emulation failed to start: \(error)")
self.isRunning = false
return
}
}
while self.isRunning && runLoop.run(mode: .default, before: .distantFuture) {
autoreleasepool { }
}
Self.log("Emulation loop ended")
}
emulationThread?.name = "RyujinxEmulationThread"
emulationThread?.qualityOfService = .userInteractive
emulationThread?.threadPriority = 0.9
emulationThread?.start()
}
func quickStart(romPath: String) throws {
let config = Configuration(inputPath: romPath)
try startWithRunLoop(config: config)
}
/// Stops the emulator
func stop() throws {
guard isRunning else {
throw RyujinxError.notRunning
}
isRunning = false
emulationThread?.cancel()
emulationThread = nil
}
var running: Bool {
return isRunning
}
static func log(_ message: String) {
print("[Ryujinx] \(message)")
}
}
extension RyujinxEmulator.Configuration {
var toCommandLineArgs: [String] {
var args: [String] = []
args.append(inputPath)
// if enableKeyboard {
// args.append("--enable-keyboard")
// }
args.append("--graphics-backend")
args.append(graphicsBackend)
args.append(contentsOf: additionalArgs)
return args
}
/// Create configuration from command line arguments
static func fromCommandLineArgs(_ args: [String]) -> RyujinxEmulator.Configuration? {
var inputPath: String?
var enableKeyboard = false
var graphicsBackend = "Vulkan"
var additionalArgs: [String] = []
var i = 0
while i < args.count {
switch args[i] {
case "--enable-keyboard":
enableKeyboard = true
case "--graphics-backend":
i += 1
if i < args.count {
graphicsBackend = args[i]
}
default:
additionalArgs.append(args[i])
}
i += 1
}
guard let inputPath = inputPath else {
return nil
}
return RyujinxEmulator.Configuration(
inputPath: inputPath,
mainThread: enableKeyboard,
graphicsBackend: graphicsBackend,
additionalArgs: additionalArgs
)
}
}
// MARK: - Code Taken from POC
var g_HookMmapReserved4GB: UnsafeMutableRawPointer! = nil
var g_HookMmapReservedJitCache: UnsafeMutableRawPointer! = nil
func initHookMmap() -> Bool {
// Hack: if out of memory, you can reserve less (e.g. around 0xc000_0000 or even 0x8000_0000) but it'll crash later
let reserve4GBSize = 0x1_0000_0000
g_HookMmapReserved4GB = mmap(
nil, reserve4GBSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)
if g_HookMmapReserved4GB == MAP_FAILED {
print("can't allocate 4gb")
return false
}
let reserveJitCacheSize = 0x8000_0000
g_HookMmapReservedJitCache = mmap(
nil, reserveJitCacheSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)
if g_HookMmapReservedJitCache == MAP_FAILED {
print("can't allocate jit cache")
return false
}
if !reallocateAreaWithOwnership(address: g_HookMmapReserved4GB, size: reserve4GBSize) {
print("can't reallocate area with ownership for 4gb")
return false
}
if !reallocateAreaWithOwnership(address: g_HookMmapReservedJitCache, size: reserveJitCacheSize) {
print("can't reallocate area with ownership for jitcache")
return false
}
print("Allocated Needed Ram")
return true
}
func hookMmap(
addr: UnsafeMutableRawPointer?, len: Int, prot: Int32, flags: Int32, fd: Int32, offset: off_t
) -> UnsafeMutableRawPointer! {
print("mmap hook! \(String(describing: addr)) \(len) \(prot) \(flags)")
// TODO(zhuowei): threads?
if g_HookMmapReserved4GB != nil && len == 0x1_0000_0000 {
let ret = g_HookMmapReserved4GB
g_HookMmapReserved4GB = nil
print("returning 4gb: \(ret!)")
return ret
}
if g_HookMmapReservedJitCache != nil && len == 0x7ff0_0000 {
// Hack: it wants 2GB; give it smaller
let ret = g_HookMmapReservedJitCache
g_HookMmapReservedJitCache = nil
print("returning jitcache: \(ret!)")
return ret
}
return mmap(addr, len, prot, flags, fd, offset)
}
func reallocateAreaWithOwnership(address: UnsafeMutableRawPointer, size: Int) -> Bool {
let addressBase: mach_vm_address_t = mach_vm_address_t(UInt(bitPattern: address))
let mapChunkSize = 128 * 1024 * 1024
for off in stride(from: 0, to: size, by: mapChunkSize) {
let targetSize = memory_object_size_t(min(mapChunkSize, size - off))
var memoryObjectSize = targetSize
var memoryObjectPort: mach_port_t = 0
let err = mach_make_memory_entry_64(
mach_task_self_, &memoryObjectSize, 0,
MAP_MEM_NAMED_CREATE | MAP_MEM_LEDGER_TAGGED | VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE,
&memoryObjectPort, /*parent_entry=*/ 0)
if err != 0 {
print("mach_make_memory_entry_64 returned error: \(String(cString: mach_error_string(err)!))")
return false
}
defer { mach_port_deallocate(mach_task_self_, memoryObjectPort) }
if memoryObjectSize != targetSize {
print("size is wrong?! \(memoryObjectSize) \(targetSize)")
return false
}
let err2 = mach_memory_entry_ownership(
memoryObjectPort, TASK_NULL, VM_LEDGER_TAG_DEFAULT, VM_LEDGER_FLAG_NO_FOOTPRINT)
if err2 != 0 {
print(
"mach_memory_entry_ownership returned error: \(String(cString: mach_error_string(err2)!))")
return false
}
let targetMapAddress: vm_address_t = vm_address_t(addressBase) + vm_address_t(off)
var mapAddress = targetMapAddress
let err3 = vm_map(
mach_task_self_, &mapAddress, vm_size_t(memoryObjectSize), /*mask=*/ 0, /*flags=*/
VM_FLAGS_OVERWRITE,
memoryObjectPort, /*offset=*/ 0, /*copy=*/ 0, VM_PROT_READ | VM_PROT_WRITE,
VM_PROT_READ | VM_PROT_WRITE | VM_PROT_EXECUTE, VM_INHERIT_COPY)
if err3 != 0 {
print("vm_map returned error: \(String(cString: mach_error_string(err3)!))")
return false
}
if mapAddress != targetMapAddress {
print("map address wrong")
return false
}
}
return true
}
typealias SystemNative_Open_Type = @convention(c) (
_ path: UnsafePointer<CChar>, _ flags: Int32, _ mode: Int32
) -> Int
var real_SystemNative_Open: SystemNative_Open_Type!
func hook_SystemNative_Open(path: UnsafePointer<CChar>, flags: Int32, mode: Int32) -> Int {
let fileName = String(cString: path)
print("opening \(fileName)")
return real_SystemNative_Open(path, flags, mode)
}
func pInvokeOverride(libraryName: UnsafePointer<CChar>!, entrypointName: UnsafePointer<CChar>!)
-> UnsafeRawPointer?
{
let libraryName = String(cString: libraryName)
let entrypointName = String(cString: entrypointName)
// print(libraryName, entrypointName)
if entrypointName == "mmap" {
typealias MmapType = @convention(c) (
_: UnsafeMutableRawPointer?, _: Int, _: Int32, _: Int32, _: Int32, _: off_t
) -> UnsafeMutableRawPointer?
return unsafeBitCast(hookMmap as MmapType, to: UnsafeRawPointer.self)
} else if entrypointName == "SystemNative_Open" {
let handle = dlopen("libSystem.Native.dylib", RTLD_LOCAL | RTLD_LAZY)
real_SystemNative_Open = unsafeBitCast(
dlsym(handle, "SystemNative_Open"), to: SystemNative_Open_Type.self)
return unsafeBitCast(
hook_SystemNative_Open as SystemNative_Open_Type, to: UnsafeRawPointer.self)
}
return nil
}