From 4541c097b13bcc1e342b5b97448925282542c0ac Mon Sep 17 00:00:00 2001 From: James Hart Date: Mon, 20 Jul 2026 11:01:35 +1000 Subject: [PATCH] Add support for x86_64-only cores for arm64 builds --- OpenEmuKit.xcodeproj/project.pbxproj | 4 ++ Source/NSXPCConnection+HelperApp.swift | 10 ++-- Source/OECorePlugin.swift | 33 ++++++++++++ Source/OEXPCGameCoreManager.swift | 6 ++- Source/Platform.swift | 70 ++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 Source/Platform.swift diff --git a/OpenEmuKit.xcodeproj/project.pbxproj b/OpenEmuKit.xcodeproj/project.pbxproj index 905c917..16e823d 100644 --- a/OpenEmuKit.xcodeproj/project.pbxproj +++ b/OpenEmuKit.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 05EEF0F22707C875008A03DC /* ShaderPresetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05EEF0F12707C875008A03DC /* ShaderPresetData.swift */; }; 05EEF1022707C986008A03DC /* ShaderPresetStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05EEF1012707C986008A03DC /* ShaderPresetStoreTests.swift */; }; 05EEF1052707CA9B008A03DC /* OpenEmuKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05E6957F24CA5D4200ACFB35 /* OpenEmuKit.framework */; }; + 8BDFCA8C2FFF2266009A0672 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BDFCA8B2FFF2266009A0672 /* Platform.swift */; }; 8F3A1837285C9F88008A7AC9 /* NSBundle+CacheFlushing.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F3A1836285C9F0E008A7AC9 /* NSBundle+CacheFlushing.m */; }; DD75EE5D298976C60056B3BA /* MTL3DGameRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD75EE5C298976C60056B3BA /* MTL3DGameRenderer.swift */; }; /* End PBXBuildFile section */ @@ -191,6 +192,7 @@ 05EEF0F72707C96B008A03DC /* OpenEmuKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenEmuKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 05EEF1012707C986008A03DC /* ShaderPresetStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShaderPresetStoreTests.swift; sourceTree = ""; }; 05EEF1032707CA7A008A03DC /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 8BDFCA8B2FFF2266009A0672 /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = ""; }; 8F3A1836285C9F0E008A7AC9 /* NSBundle+CacheFlushing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSBundle+CacheFlushing.m"; sourceTree = ""; }; 8F4AEABF27354776005C6246 /* NSBundle+CacheFlushing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSBundle+CacheFlushing.h"; sourceTree = ""; }; DD75EE5C298976C60056B3BA /* MTL3DGameRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MTL3DGameRenderer.swift; sourceTree = ""; }; @@ -438,6 +440,7 @@ 05E6BA3324CCD78300ACFB35 /* Utilities */ = { isa = PBXGroup; children = ( + 8BDFCA8B2FFF2266009A0672 /* Platform.swift */, 05C40F8926F6A72F0072B722 /* ApproximatelyEqual.swift */, 0547C20B2707F60500D386D3 /* Crypto.swift */, 057E7FEC28755CB100BC1C04 /* Logging.swift */, @@ -666,6 +669,7 @@ 05E6BA1D24CCD71D00ACFB35 /* OESystemPlugin.swift in Sources */, 0572A426287B969500AC32F8 /* OEGameCoreOwner.swift in Sources */, 0572A4012877883100AC32F8 /* OpenEmuXPCHelperApp.swift in Sources */, + 8BDFCA8C2FFF2266009A0672 /* Platform.swift in Sources */, 0572A419287A334600AC32F8 /* OEXPCGameCoreManager.swift in Sources */, 057E7FED28755CB100BC1C04 /* Logging.swift in Sources */, 057E7FF128762D5400BC1C04 /* OpenGL3GameRenderer.swift in Sources */, diff --git a/Source/NSXPCConnection+HelperApp.swift b/Source/NSXPCConnection+HelperApp.swift index 47d580d..1cc737e 100644 --- a/Source/NSXPCConnection+HelperApp.swift +++ b/Source/NSXPCConnection+HelperApp.swift @@ -37,15 +37,16 @@ extension NSXPCConnection { static let helperServiceNameArgumentPrefix = "--org.openemu.broker.name=" private static var xpcTaskKey = 0 - static func makeConnection(serviceName name: String, executableURL url: URL) throws -> NSXPCConnection { + static func makeConnection(serviceName name: String, executableURL url: URL, architecture arch: String) throws -> NSXPCConnection { let identifier = UUID().uuidString + let fullServiceName = name + "." + arch /// 1. Launch Helper App /// This results in the helper app establishing a connection to the broker and registering its /// `identifier` let task = Process() - task.executableURL = url - task.arguments = ["\(Self.helperIdentifierArgumentPrefix)\(identifier)", "\(Self.helperServiceNameArgumentPrefix)\(name)"] + task.executableURL = URL(fileURLWithPath: "/usr/bin/arch") + task.arguments = ["-\(arch)", url.path, "\(Self.helperIdentifierArgumentPrefix)\(identifier)", "\(Self.helperServiceNameArgumentPrefix)\(fullServiceName)"] task.terminationHandler = { task in os_log(.error, log: .helper, "Helper terminated unexpectedly. { id = %{public}@, reason = %ld, exit = %d }", @@ -55,6 +56,7 @@ extension NSXPCConnection { } task.standardError = FileHandle.standardError task.standardOutput = FileHandle.standardOutput + do { try task.run() } catch { @@ -74,7 +76,7 @@ extension NSXPCConnection { } /// 2. Launch a connection to the broker - let cn = NSXPCConnection(serviceName: name) + let cn = NSXPCConnection(serviceName: fullServiceName) cn.invalidationHandler = { os_log(.error, log: .helper, "Broker connection was unexpectedely invalidated.") } diff --git a/Source/OECorePlugin.swift b/Source/OECorePlugin.swift index 7e0df58..b0fea8f 100644 --- a/Source/OECorePlugin.swift +++ b/Source/OECorePlugin.swift @@ -24,6 +24,7 @@ import Foundation import OpenEmuBase +import os.log public class OECorePlugin: OEPlugin { @@ -214,6 +215,38 @@ public class OECorePlugin: OEPlugin { } } } + + private func isCodeSigned(forArchitecture architecture: OECorePlugin.Architecture) -> Bool { + var code: SecStaticCode? + let attributes: [String: Any] = [kSecCodeAttributeArchitecture as String: architecture] + + // Create a code object for the bundle + let result = SecStaticCodeCreateWithPathAndAttributes(url as CFURL, [], attributes as CFDictionary, &code) + if let code = code, + result == errSecSuccess { + + // Check the code signature + let validityResult = SecStaticCodeCheckValidity(code, [], nil) + if validityResult == errSecSuccess { + return true + } else { + os_log(.debug, log: .default, "Code sign check failed. bundle =%@, arch =%@, error =%@", url.absoluteString, architecture, SecCopyErrorMessageString(validityResult, nil) as String? ?? "") + return false + } + } + + os_log(.debug, log: .default, "Code sign check failed. bundle =%@, arch =%@, error =%@", url.absoluteString, architecture, SecCopyErrorMessageString(result, nil) as String? ?? "") + return false + } + + public func canRunArchitecture(_ architecture: OECorePlugin.Architecture) -> Bool { + if architecture == .x86_64 { + return self.architectures.contains(.x86_64) && (Platform.isRosettaAvailable || Platform.isIntelX86) + } else if architecture == .arm64 { + return self.architectures.contains(.arm64) && self.isCodeSigned(forArchitecture: .arm64) && Platform.isAppleSilicon + } + return false + } } public extension OECorePlugin { diff --git a/Source/OEXPCGameCoreManager.swift b/Source/OEXPCGameCoreManager.swift index 180b33d..24260fd 100644 --- a/Source/OEXPCGameCoreManager.swift +++ b/Source/OEXPCGameCoreManager.swift @@ -29,10 +29,12 @@ import OpenEmuKitPrivate @objc public class OEXPCGameCoreManager: GameCoreManager { let serviceName: String let helperExecutableName: String + let architecture: OECorePlugin.Architecture - @objc public init(startupInfo: OEGameStartupInfo, gameCoreOwner: OEGameCoreOwner, serviceName: String, helperExecutableName: String) { + @objc public init(startupInfo: OEGameStartupInfo, gameCoreOwner: OEGameCoreOwner, serviceName: String, helperExecutableName: String, architecture: OECorePlugin.Architecture) { self.serviceName = serviceName self.helperExecutableName = helperExecutableName + self.architecture = architecture super.init(startupInfo: startupInfo, gameCoreOwner: gameCoreOwner) } @@ -49,7 +51,7 @@ import OpenEmuKitPrivate let cn: NSXPCConnection do { - cn = try .makeConnection(serviceName: serviceName, executableURL: executableURL) + cn = try .makeConnection(serviceName: serviceName, executableURL: executableURL, architecture: architecture) helperConnection = cn } catch { DispatchQueue.main.async { diff --git a/Source/Platform.swift b/Source/Platform.swift new file mode 100644 index 0000000..108806b --- /dev/null +++ b/Source/Platform.swift @@ -0,0 +1,70 @@ +// Copyright (c) 2026, OpenEmu Team +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of the OpenEmu Team nor the +// names of its contributors may be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY OpenEmu Team ''AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL OpenEmu Team BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import Foundation + +public class Platform { + + public static var isAppleSilicon: Bool { + var cpu: cpu_type_t = 0 + var size = MemoryLayout.size + + let res = sysctlbyname("hw.cputype", &cpu, &size, nil, 0) + guard res == 0 else { return false } + + // Check whether the CPU is ARM-family (or that Rosetta has translated) + return ((UInt32(cpu) & ~CPU_ARCH_MASK) == cpu_type_t(CPU_TYPE_ARM)) || isRunningUnderRosetta() + } + + public static var isIntelX86: Bool { + var cpu: cpu_type_t = 0 + var size = MemoryLayout.size + + let res = sysctlbyname("hw.cputype", &cpu, &size, nil, 0) + guard res == 0 else { return false } + + // Check whether the CPU is X86-family (and that Rosetta hasn't translated, since + // sysctlbyname("hw.cputype", ..) will return CPU_TYPE_X86 for Rosetta translated processes) + return ((UInt32(cpu) & ~CPU_ARCH_MASK) == cpu_type_t(CPU_TYPE_X86)) && !isRunningUnderRosetta() + } + + public static var isRosettaAvailable: Bool { + if #unavailable(macOS 28.0), isAppleSilicon { + return true // Rosetta 2 is supported on Apple Silicon Macs on macOS 27 and lower + } else { + return false + } + } + + + private static func isRunningUnderRosetta() -> Bool { + var procTranslated: Int32 = 0 + var size = MemoryLayout.size + + let res = sysctlbyname("sysctl.proc_translated", &procTranslated, &size, nil, 0) + guard res == 0 else { return false } + + return procTranslated == 1 + } +}