├── DeltaCore ├── include │ ├── DeltaTypes.h │ └── DLTAMuteSwitchMonitor.h ├── Supporting Files │ ├── KeyboardGameController.deltamapping │ ├── Info.plist │ └── MFiGameController.deltamapping ├── Protocols │ ├── Model │ │ ├── CheatProtocol.swift │ │ ├── SaveStateProtocol.swift │ │ ├── GameControllerInputMappingProtocol.swift │ │ ├── GameProtocol.swift │ │ └── ControllerSkinProtocol.swift │ ├── Rendering │ │ ├── AudioRendering.swift │ │ └── VideoRendering.swift │ └── Inputs │ │ ├── Input.swift │ │ └── GameController.swift ├── Model │ ├── Cheat.swift │ ├── Game.swift │ ├── SaveState.swift │ ├── GameControllerInputMapping.swift │ ├── Inputs │ │ ├── StandardGameControllerInput.swift │ │ └── AnyInput.swift │ ├── CheatFormat.swift │ ├── ControllerSkinTraits.swift │ └── GameControllerStateManager.swift ├── DeltaTypes.m ├── Extensions │ ├── UIApplication+AppExtension.swift │ ├── UIWindowScene+StageManager.swift │ ├── Bundle+Resources.swift │ ├── UIResponder+FirstResponder.swift │ ├── UIScreen+ControllerSkin.swift │ ├── CharacterSet+Hexadecimals.swift │ ├── ProcessInfo+visionOS.swift │ ├── UIDevice+Vibration.swift │ ├── Thread+RealTime.swift │ ├── CGGeometry+Conveniences.swift │ ├── UIImage+PDF.swift │ └── UIScene+KeyboardFocus.swift ├── Emulator Core │ ├── Audio │ │ ├── DLTAMuteSwitchMonitor.h │ │ ├── DLTAMuteSwitchMonitor.m │ │ ├── RingBuffer.swift │ │ └── AudioManager.swift │ └── Video │ │ ├── VideoFormat.swift │ │ ├── RenderThread.swift │ │ ├── BitmapProcessor.swift │ │ ├── OpenGLESProcessor.swift │ │ └── VideoManager.swift ├── UI │ ├── Controller │ │ ├── ImmediatePanGestureRecognizer.swift │ │ ├── TouchInputView.swift │ │ ├── ControllerInputView.swift │ │ ├── ControllerDebugView.swift │ │ ├── TouchControllerSkin.swift │ │ ├── ButtonsInputView.swift │ │ └── ThumbstickInputView.swift │ └── Game │ │ ├── GameWindow.swift │ │ └── GameView.swift ├── DeltaCore.h ├── DeltaTypes.h ├── Cores │ ├── EmulatorBridging.swift │ └── DeltaCoreProtocol.swift ├── Delta.swift ├── Types │ └── ExtensibleEnums.swift ├── Filters │ └── FilterChain.swift └── Game Controllers │ ├── Keyboard │ ├── KeyboardResponder.swift │ └── KeyboardGameController.swift │ ├── ExternalGameControllerManager.swift │ └── MFi │ └── MFiGameController.swift ├── .gitmodules ├── DeltaCore.xcodeproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── DeltaCore.xcscmblueprint ├── .gitignore ├── DeltaCore.podspec └── Package.swift /DeltaCore/include/DeltaTypes.h: -------------------------------------------------------------------------------- 1 | ../DeltaTypes.h -------------------------------------------------------------------------------- /DeltaCore/include/DLTAMuteSwitchMonitor.h: -------------------------------------------------------------------------------- 1 | ../Emulator Core/Audio/DLTAMuteSwitchMonitor.h -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "External/ZIPFoundation"] 2 | path = External/ZIPFoundation 3 | url = https://github.com/rileytestut/ZIPFoundation.git 4 | -------------------------------------------------------------------------------- /DeltaCore/Supporting Files/KeyboardGameController.deltamapping: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rileytestut/DeltaCore/HEAD/DeltaCore/Supporting Files/KeyboardGameController.deltamapping -------------------------------------------------------------------------------- /DeltaCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Model/CheatProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheatProtocol.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 5/19/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol CheatProtocol 12 | { 13 | var code: String { get } 14 | var type: CheatType { get } 15 | } 16 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Model/SaveStateProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SaveStateProtocol.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 1/31/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol SaveStateProtocol 12 | { 13 | var fileURL: URL { get } 14 | var gameType: GameType { get } 15 | } 16 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Rendering/AudioRendering.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioRendering.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/29/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc(DLTAAudioRendering) 12 | public protocol AudioRendering: NSObjectProtocol 13 | { 14 | var audioBuffer: RingBuffer { get } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | # 3 | *.DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Swift Package Manager 25 | /.swiftpm 26 | /Packages 27 | -------------------------------------------------------------------------------- /DeltaCore/Model/Cheat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cheat.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 5/19/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Cheat: CheatProtocol 12 | { 13 | public var code: String 14 | public var type: CheatType 15 | 16 | public init(code: String, type: CheatType) 17 | { 18 | self.code = code 19 | self.type = type 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DeltaCore/Model/Game.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Game.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/20/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Game: GameProtocol 12 | { 13 | public var fileURL: URL 14 | public var type: GameType 15 | 16 | public init(fileURL: URL, type: GameType) 17 | { 18 | self.fileURL = fileURL 19 | self.type = type 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Model/GameControllerInputMappingProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameControllerInputMappingProtocol.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 8/14/17. 6 | // Copyright © 2017 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol GameControllerInputMappingProtocol 12 | { 13 | var gameControllerInputType: GameControllerInputType { get } 14 | 15 | func input(forControllerInput controllerInput: Input) -> Input? 16 | } 17 | -------------------------------------------------------------------------------- /DeltaCore/Model/SaveState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SaveState.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 1/31/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct SaveState: SaveStateProtocol 12 | { 13 | public var fileURL: URL 14 | public var gameType: GameType 15 | 16 | public init(fileURL: URL, gameType: GameType) 17 | { 18 | self.fileURL = fileURL 19 | self.gameType = gameType 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DeltaCore/DeltaTypes.m: -------------------------------------------------------------------------------- 1 | // 2 | // DeltaTypes.m 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/30/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | #import "DeltaTypes.h" 10 | 11 | @import Foundation; 12 | 13 | NSNotificationName const DeltaRegistrationRequestNotification = @"DeltaRegistrationRequestNotification"; 14 | 15 | EmulatorCoreOption const EmulatorCoreOptionOpenGLES2 = @"DLTAEmulatorCoreOptionOpenGLES2"; 16 | EmulatorCoreOption const EmulatorCoreOptionMetal = @"DLTAEmulatorCoreOptionMetal"; 17 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Rendering/VideoRendering.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoRendering.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/29/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | @objc(DLTAVideoRendering) 13 | public protocol VideoRendering: NSObjectProtocol 14 | { 15 | var videoBuffer: UnsafeMutablePointer? { get } 16 | 17 | var viewport: CGRect { get set } 18 | 19 | func prepare() 20 | func processFrame() 21 | } 22 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/UIApplication+AppExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+AppExtension.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/14/18. 6 | // Copyright © 2018 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIApplication 12 | { 13 | // Cannot normally use UIApplication.shared from extensions, so we get around this by calling value(forKey:). 14 | class var delta_shared: UIApplication? { 15 | return UIApplication.value(forKey: "sharedApplication") as? UIApplication 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Audio/DLTAMuteSwitchMonitor.h: -------------------------------------------------------------------------------- 1 | // 2 | // DLTAMuteSwitchMonitor.h 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 11/19/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface DLTAMuteSwitchMonitor : NSObject 14 | 15 | @property (nonatomic, readonly) BOOL isMonitoring; 16 | @property (nonatomic, readonly) BOOL isMuted; 17 | 18 | - (void)startMonitoring:(void (^)(BOOL isMuted))muteHandler; 19 | - (void)stopMonitoring; 20 | 21 | @end 22 | 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /DeltaCore/UI/Controller/ImmediatePanGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImmediatePanGestureRecognizer.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 8/5/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit.UIGestureRecognizerSubclass 10 | 11 | class ImmediatePanGestureRecognizer: UIPanGestureRecognizer 12 | { 13 | override func touchesBegan(_ touches: Set, with event: UIEvent) 14 | { 15 | guard self.state != .began else { return } 16 | 17 | super.touchesBegan(touches, with: event) 18 | 19 | self.state = .began 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Model/GameProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameProtocol.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 3/8/15. 6 | // Copyright (c) 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol GameProtocol 12 | { 13 | var fileURL: URL { get } 14 | var gameSaveURL: URL { get } 15 | 16 | var type: GameType { get } 17 | } 18 | 19 | public extension GameProtocol 20 | { 21 | var gameSaveURL: URL { 22 | let fileExtension = Delta.core(for: self.type)?.gameSaveFileExtension ?? "sav" 23 | 24 | let gameURL = self.fileURL.deletingPathExtension() 25 | let gameSaveURL = gameURL.appendingPathExtension(fileExtension) 26 | return gameSaveURL 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/UIWindowScene+StageManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIWindowScene+StageManager.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 8/1/22. 6 | // Copyright © 2022 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @objc private protocol UIWindowScenePrivate: NSObjectProtocol 12 | { 13 | var _enhancedWindowingEnabled: Bool { get } 14 | } 15 | 16 | @available(iOS 16, *) 17 | extension UIWindowScene 18 | { 19 | @_spi(Internal) 20 | public var isStageManagerEnabled: Bool { 21 | guard self.responds(to: #selector(getter: UIWindowScenePrivate._enhancedWindowingEnabled)) else { return false } 22 | 23 | let windowScene = unsafeBitCast(self, to: UIWindowScenePrivate.self) 24 | let isStageManagerEnabled = windowScene._enhancedWindowingEnabled 25 | return isStageManagerEnabled 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DeltaCore/DeltaCore.h: -------------------------------------------------------------------------------- 1 | // 2 | // DeltaCore.h 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 3/8/15. 6 | // Copyright (c) 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for DeltaCore. 12 | FOUNDATION_EXPORT double DeltaCoreVersionNumber; 13 | 14 | //! Project version string for DeltaCore. 15 | FOUNDATION_EXPORT const unsigned char DeltaCoreVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | #import 19 | #import 20 | 21 | // HACK: Needed because the generated DeltaCore-Swift header file uses @import syntax, which isn't supported in Objective-C++ code. 22 | #import 23 | #import 24 | #import 25 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/Bundle+Resources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+Resources.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 2/3/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bundle 12 | { 13 | class var resources: Bundle { 14 | #if FRAMEWORK 15 | let bundle = Bundle(for: RingBuffer.self) 16 | #elseif SWIFT_PACKAGE 17 | let bundle = Bundle.module 18 | #elseif STATIC_LIBRARY 19 | let bundle: Bundle 20 | if let bundleURL = Bundle.main.url(forResource: "DeltaCore", withExtension: "bundle") 21 | { 22 | bundle = Bundle(url: bundleURL)! 23 | } 24 | else 25 | { 26 | bundle = .main 27 | } 28 | #else 29 | let bundle = Bundle.main 30 | #endif 31 | 32 | return bundle 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/UIResponder+FirstResponder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIResponder+FirstResponder.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/14/18. 6 | // Copyright © 2018 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private class FirstResponderEvent: UIEvent 12 | { 13 | var firstResponder: UIResponder? 14 | } 15 | 16 | extension UIResponder 17 | { 18 | @objc(delta_firstResponder) 19 | class var firstResponder: UIResponder? { 20 | let event = FirstResponderEvent() 21 | UIApplication.delta_shared?.sendAction(#selector(UIResponder.findFirstResponder(sender:event:)), to: nil, from: nil, for: event) 22 | return event.firstResponder 23 | } 24 | 25 | @objc(delta_findFirstResponderWithSender:event:) 26 | private func findFirstResponder(sender: Any?, event: FirstResponderEvent) 27 | { 28 | event.firstResponder = self 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /DeltaCore/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/UIScreen+ControllerSkin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScreen+ControllerSkin.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/4/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIScreen 12 | { 13 | var defaultControllerSkinSize: ControllerSkin.Size 14 | { 15 | let fixedBounds = self.fixedCoordinateSpace.convert(self.bounds, from: self.coordinateSpace) 16 | 17 | if UIDevice.current.userInterfaceIdiom == .pad 18 | { 19 | switch fixedBounds.width 20 | { 21 | case (...768): return .small 22 | case (...834): return .medium 23 | default: return .large 24 | } 25 | } 26 | else 27 | { 28 | switch fixedBounds.width 29 | { 30 | case 320: return .small 31 | case 375: return .medium 32 | default: return .large 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/CharacterSet+Hexadecimals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CharacterSet+Hexadecimals.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 4/30/17. 6 | // Copyright © 2017 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Extend NSCharacterSet for Objective-C interopability. 12 | public extension NSCharacterSet 13 | { 14 | @objc(hexadecimalCharacterSet) 15 | class var hexadecimals: NSCharacterSet 16 | { 17 | let characterSet = NSCharacterSet(charactersIn: "0123456789ABCDEFabcdef") 18 | return characterSet 19 | } 20 | } 21 | 22 | public extension NSMutableCharacterSet 23 | { 24 | @objc(hexadecimalCharacterSet) 25 | override class var hexadecimals: NSMutableCharacterSet 26 | { 27 | let characterSet = NSCharacterSet.hexadecimals.mutableCopy() as! NSMutableCharacterSet 28 | return characterSet 29 | } 30 | } 31 | 32 | public extension CharacterSet 33 | { 34 | static var hexadecimals: CharacterSet 35 | { 36 | return NSCharacterSet.hexadecimals as CharacterSet 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DeltaCore/UI/Game/GameWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameWindow.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 8/11/22. 6 | // Copyright © 2022 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class GameWindow: UIWindow 12 | { 13 | override open func _restoreFirstResponder() 14 | { 15 | guard #available(iOS 16, *), let firstResponder = self._lastFirstResponder else { return super._restoreFirstResponder() } 16 | 17 | if firstResponder is ControllerView 18 | { 19 | // HACK: iOS 16 beta 5 aggressively tries to restore ControllerView as first responder, even when we've explicitly resigned it as first responder. 20 | // This can result in the keyboard controller randomly appearing even when user is using another app in the foreground with Stage Manager. 21 | // As a workaround, we just ignore _restoreFirstResponder() calls when ControllerView was the last first responder and manage it ourselves. 22 | return 23 | } 24 | 25 | return super._restoreFirstResponder() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Video/VideoFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoBufferInfo.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 4/18/17. 6 | // Copyright © 2017 Riley Testut. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | import CoreImage 11 | 12 | extension VideoFormat 13 | { 14 | public enum Format: Equatable 15 | { 16 | case bitmap(PixelFormat) 17 | case openGLES2 18 | case openGLES3 19 | } 20 | 21 | public enum PixelFormat: Equatable 22 | { 23 | case rgb565 24 | case bgra8 25 | case rgba8 26 | 27 | public var bytesPerPixel: Int { 28 | switch self 29 | { 30 | case .rgb565: return 2 31 | case .bgra8: return 4 32 | case .rgba8: return 4 33 | } 34 | } 35 | } 36 | } 37 | 38 | public struct VideoFormat: Equatable 39 | { 40 | public var format: Format 41 | public var dimensions: CGSize 42 | 43 | public init(format: Format, dimensions: CGSize) 44 | { 45 | self.format = format 46 | self.dimensions = dimensions 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DeltaCore/DeltaTypes.h: -------------------------------------------------------------------------------- 1 | // 2 | // DeltaTypes.h 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 1/30/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | NS_HEADER_AUDIT_BEGIN(nullability, sendability) 13 | 14 | // Extensible Enums 15 | typedef NSString *GameType NS_TYPED_EXTENSIBLE_ENUM; 16 | typedef NSString *CheatType NS_TYPED_EXTENSIBLE_ENUM; 17 | typedef NSString *GameControllerInputType NS_TYPED_EXTENSIBLE_ENUM; 18 | 19 | // Emulator Core Options 20 | typedef NSString *EmulatorCoreOption NS_REFINED_FOR_SWIFT NS_TYPED_EXTENSIBLE_ENUM; 21 | FOUNDATION_EXPORT EmulatorCoreOption const EmulatorCoreOptionOpenGLES2; 22 | FOUNDATION_EXPORT EmulatorCoreOption const EmulatorCoreOptionMetal; 23 | 24 | extern NSNotificationName const DeltaRegistrationRequestNotification; 25 | 26 | // Used by GameWindow. 27 | @interface UIWindow (Private) 28 | 29 | @property (nullable, weak, nonatomic, setter=_setLastFirstResponder:) UIResponder *_lastFirstResponder /* API_AVAILABLE(ios(16)) */; 30 | - (void)_restoreFirstResponder /* API_AVAILABLE(ios(16)) */; 31 | 32 | @end 33 | 34 | NS_HEADER_AUDIT_END(nullability, sendability) 35 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/ProcessInfo+visionOS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessInfo+visionOS.swift 3 | // Delta 4 | // 5 | // Created by Riley Testut on 1/12/24. 6 | // Copyright © 2024 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LocalAuthentication 11 | 12 | public extension ProcessInfo 13 | { 14 | var isRunningOnVisionPro: Bool { 15 | return Self._isRunningOnVisionPro 16 | } 17 | 18 | // Somewhat expensive to calculate, which can result in dropped touch screen inputs unless we cache value. 19 | private static let _isRunningOnVisionPro: Bool = { 20 | // Returns true even when running on iOS :/ 21 | // guard #available(visionOS 1, *) else { return false } 22 | // return true 23 | 24 | let context = LAContext() 25 | _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) // Sets .biometryType when called. 26 | 27 | // Can't reference `.opticID` due to bug with #available, so check if .biometryType isn't one of the other types instead. 28 | let isRunningOnVisionPro = (context.biometryType != .faceID && context.biometryType != .touchID && context.biometryType != .none) 29 | return isRunningOnVisionPro 30 | }() 31 | } 32 | -------------------------------------------------------------------------------- /DeltaCore.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "DeltaCore" 3 | spec.version = "0.1" 4 | spec.summary = "iOS Emulator Plug-in Framework" 5 | spec.description = "iOS framework that powers Delta emulator." 6 | spec.homepage = "https://github.com/rileytestut/DeltaCore" 7 | spec.platform = :ios, "14.0" 8 | spec.source = { :git => "https://github.com/rileytestut/DeltaCore.git" } 9 | 10 | spec.author = { "Riley Testut" => "riley@rileytestut.com" } 11 | spec.social_media_url = "https://twitter.com/rileytestut" 12 | 13 | spec.source_files = "DeltaCore/**/*.{h,m,swift}" 14 | spec.exclude_files = "DeltaCore/DeltaTypes.h", "DeltaCore/Emulator Core/Audio/DLTAMuteSwitchMonitor.h" 15 | spec.public_header_files = "DeltaCore/include/*.h" 16 | spec.resource_bundles = { 17 | "DeltaCore" => ["DeltaCore/**/*.deltamapping"] 18 | } 19 | 20 | spec.dependency "ZIPFoundation" 21 | 22 | spec.xcconfig = { 23 | "SWIFT_ACTIVE_COMPILATION_CONDITIONS" => "STATIC_LIBRARY", 24 | "OTHER_CFLAGS" => "-DSTATIC_LIBRARY" 25 | } 26 | 27 | spec.script_phase = { :name => 'Copy Swift Header', :script => <<-SCRIPT 28 | target_dir=${BUILT_PRODUCTS_DIR} 29 | 30 | mkdir -p ${target_dir} 31 | 32 | # Copy any file that looks like a Swift generated header to the include path 33 | cp ${DERIVED_SOURCES_DIR}/*-Swift.h ${target_dir} 34 | SCRIPT 35 | } 36 | 37 | end 38 | -------------------------------------------------------------------------------- /DeltaCore/Cores/EmulatorBridging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmulatorBridging.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/29/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | @objc(DLTAEmulatorBridging) 12 | public protocol EmulatorBridging: NSObjectProtocol 13 | { 14 | /// State 15 | var gameURL: URL? { get } 16 | 17 | /// System 18 | var frameDuration: TimeInterval { get } 19 | 20 | /// Audio 21 | var audioRenderer: AudioRendering? { get set } 22 | 23 | /// Video 24 | var videoRenderer: VideoRendering? { get set } 25 | 26 | /// Saves 27 | var saveUpdateHandler: (() -> Void)? { get set } 28 | 29 | 30 | /// Emulation State 31 | func start(withGameURL gameURL: URL) 32 | func stop() 33 | func pause() 34 | func resume() 35 | 36 | /// Game Loop 37 | @objc(runFrameAndProcessVideo:) func runFrame(processVideo: Bool) 38 | 39 | /// Inputs 40 | func activateInput(_ input: Int, value: Double, playerIndex: Int) 41 | func deactivateInput(_ input: Int, playerIndex: Int) 42 | func resetInputs() 43 | 44 | /// Save States 45 | @objc(saveSaveStateToURL:) func saveSaveState(to url: URL) 46 | @objc(loadSaveStateFromURL:) func loadSaveState(from url: URL) 47 | 48 | /// Game Games 49 | @objc(saveGameSaveToURL:) func saveGameSave(to url: URL) 50 | @objc(loadGameSaveFromURL:) func loadGameSave(from url: URL) 51 | 52 | /// Cheats 53 | @discardableResult func addCheatCode(_ cheatCode: String, type: String) -> Bool 54 | func resetCheats() 55 | func updateCheats() 56 | 57 | @objc(readMemoryAtAddress:size:) optional func readMemory(at address: Int, size: Int) -> Data? 58 | } 59 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/UIDevice+Vibration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+Vibration.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 11/28/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AudioToolbox 11 | 12 | public extension UIDevice 13 | { 14 | enum FeedbackSupportLevel: Int 15 | { 16 | case unsupported // iPhone 6 or earlier, or non-iPhone (e.g. iPad) 17 | case basic // iPhone 6s 18 | case feedbackGenerator // iPhone 7 and later 19 | } 20 | } 21 | 22 | public extension UIDevice 23 | { 24 | var feedbackSupportLevel: FeedbackSupportLevel 25 | { 26 | guard let rawValue = self.value(forKey: "_feedbackSupportLevel") as? Int else { return .unsupported } 27 | 28 | let feedbackSupportLevel = FeedbackSupportLevel(rawValue: rawValue) 29 | return feedbackSupportLevel ?? .feedbackGenerator // We'll assume raw values greater than 2 still support UIFeedbackGenerator ¯\_(ツ)_/¯ 30 | } 31 | 32 | var isVibrationSupported: Bool { 33 | #if (arch(i386) || arch(x86_64)) 34 | // Return false for iOS simulator 35 | return false 36 | #else 37 | // All iPhones support some form of vibration, and potentially future non-iPhone devices will support taptic feedback 38 | return (self.model.hasPrefix("iPhone")) || self.feedbackSupportLevel != .unsupported 39 | #endif 40 | } 41 | 42 | func vibrate() 43 | { 44 | guard self.isVibrationSupported else { return } 45 | 46 | switch self.feedbackSupportLevel 47 | { 48 | case .unsupported: break 49 | case .basic, .feedbackGenerator: AudioServicesPlaySystemSound(1519) // "peek" vibration 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Video/RenderThread.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RenderThread.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 1/12/21. 6 | // Copyright © 2021 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RenderThread: Thread 12 | { 13 | var action: () -> Void 14 | 15 | private let startRenderSemaphore = DispatchSemaphore(value: 0) 16 | private let finishedRenderSemaphore = DispatchSemaphore(value: 0) 17 | 18 | init(action: @escaping () -> Void) 19 | { 20 | self.action = action 21 | self.finishedRenderSemaphore.signal() 22 | 23 | super.init() 24 | 25 | self.name = "Delta - Rendering" 26 | self.qualityOfService = .userInitiated 27 | } 28 | 29 | override func main() 30 | { 31 | while !self.isCancelled 32 | { 33 | autoreleasepool { 34 | self.startRenderSemaphore.wait() 35 | defer { self.finishedRenderSemaphore.signal() } 36 | 37 | guard !self.isCancelled else { return } 38 | 39 | self.action() 40 | } 41 | } 42 | } 43 | 44 | override func cancel() 45 | { 46 | super.cancel() 47 | 48 | // We're probably waiting on startRenderSemaphore in main(), 49 | // so explicitly signal it so thread can finish. 50 | self.startRenderSemaphore.signal() 51 | } 52 | } 53 | 54 | extension RenderThread 55 | { 56 | func run() 57 | { 58 | self.startRenderSemaphore.signal() 59 | } 60 | 61 | @discardableResult 62 | func wait(timeout: DispatchTime = .distantFuture) -> DispatchTimeoutResult 63 | { 64 | return self.finishedRenderSemaphore.wait(timeout: timeout) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /DeltaCore/UI/Controller/TouchInputView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TouchInputView.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 8/4/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class TouchInputView: UIView 12 | { 13 | var valueChangedHandler: ((CGPoint?) -> Void)? 14 | 15 | override init(frame: CGRect) 16 | { 17 | super.init(frame: frame) 18 | 19 | self.isMultipleTouchEnabled = false 20 | } 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | override func touchesBegan(_ touches: Set, with event: UIEvent?) 27 | { 28 | // Manually track touches because UIPanGestureRecognizer delays touches near screen edges, 29 | // even if we've opted to de-prioritize system gestures. 30 | self.touchesMoved(touches, with: event) 31 | } 32 | 33 | override func touchesMoved(_ touches: Set, with event: UIEvent?) 34 | { 35 | guard let touch = touches.first else { return } 36 | 37 | let location = touch.location(in: self) 38 | 39 | var adjustedLocation = CGPoint(x: location.x / self.bounds.width, y: location.y / self.bounds.height) 40 | adjustedLocation.x = min(max(adjustedLocation.x, 0), 1) 41 | adjustedLocation.y = min(max(adjustedLocation.y, 0), 1) 42 | 43 | self.valueChangedHandler?(adjustedLocation) 44 | } 45 | 46 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) 47 | { 48 | self.touchesEnded(touches, with: event) 49 | } 50 | 51 | override func touchesEnded(_ touches: Set, with event: UIEvent?) 52 | { 53 | self.valueChangedHandler?(nil) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /DeltaCore/Delta.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Delta.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/22/15. 6 | // Copyright © 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if SWIFT_PACKAGE 12 | @_exported import CDeltaCore 13 | #endif 14 | 15 | extension GameType: CustomStringConvertible 16 | { 17 | public var description: String { 18 | return self.rawValue 19 | } 20 | } 21 | 22 | public extension GameType 23 | { 24 | static let unknown = GameType("com.rileytestut.delta.game.unknown") 25 | } 26 | 27 | public struct Delta 28 | { 29 | public private(set) static var registeredCores = [GameType: DeltaCoreProtocol]() 30 | 31 | private init() 32 | { 33 | } 34 | 35 | public static func register(_ core: DeltaCoreProtocol) 36 | { 37 | self.registeredCores[core.gameType] = core 38 | } 39 | 40 | public static func unregister(_ core: DeltaCoreProtocol) 41 | { 42 | // Ensure another core has not been registered for core.gameType. 43 | guard let registeredCore = self.registeredCores[core.gameType], registeredCore == core else { return } 44 | self.registeredCores[core.gameType] = nil 45 | } 46 | 47 | public static func core(for gameType: GameType) -> DeltaCoreProtocol? 48 | { 49 | return self.registeredCores[gameType] 50 | } 51 | 52 | public static var coresDirectoryURL: URL = { 53 | let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] 54 | let coresDirectoryURL = documentsDirectoryURL.appendingPathComponent("Cores", isDirectory: true) 55 | 56 | try? FileManager.default.createDirectory(at: coresDirectoryURL, withIntermediateDirectories: true, attributes: nil) 57 | 58 | return coresDirectoryURL 59 | }() 60 | } 61 | -------------------------------------------------------------------------------- /DeltaCore/Types/ExtensibleEnums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensibleEnum.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/9/18. 6 | // Copyright © 2018 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ExtensibleEnum: Hashable, Codable, RawRepresentable where RawValue == String {} 12 | 13 | public extension ExtensibleEnum 14 | { 15 | init(_ rawValue: String) 16 | { 17 | self.init(rawValue: rawValue)! 18 | } 19 | 20 | init(from decoder: Decoder) throws 21 | { 22 | let container = try decoder.singleValueContainer() 23 | 24 | let rawValue = try container.decode(String.self) 25 | self.init(rawValue: rawValue)! 26 | } 27 | 28 | func encode(to encoder: Encoder) throws 29 | { 30 | var container = encoder.singleValueContainer() 31 | try container.encode(self.rawValue) 32 | } 33 | } 34 | 35 | #if FRAMEWORK || STATIC_LIBRARY || SWIFT_PACKAGE 36 | 37 | // Conform types to ExtensibleEnum to receive automatic Codable conformance + implementation. 38 | extension GameType: ExtensibleEnum {} 39 | extension CheatType: ExtensibleEnum {} 40 | extension GameControllerInputType: ExtensibleEnum {} 41 | 42 | #else 43 | 44 | public struct GameType: ExtensibleEnum 45 | { 46 | public let rawValue: String 47 | 48 | public init(rawValue: String) 49 | { 50 | self.rawValue = rawValue 51 | } 52 | } 53 | 54 | public struct CheatType: ExtensibleEnum 55 | { 56 | public let rawValue: String 57 | 58 | public init(rawValue: String) 59 | { 60 | self.rawValue = rawValue 61 | } 62 | } 63 | 64 | public struct GameControllerInputType: ExtensibleEnum 65 | { 66 | public let rawValue: String 67 | 68 | public init(rawValue: String) 69 | { 70 | self.rawValue = rawValue 71 | } 72 | } 73 | 74 | #endif 75 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/Thread+RealTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Thread+RealTime.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/29/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Darwin.Mach 11 | 12 | private let machToSeconds: Double = { 13 | var base = mach_timebase_info() 14 | mach_timebase_info(&base) 15 | return 1e-9 * Double(base.numer) / Double(base.denom) 16 | }() 17 | 18 | internal extension Thread 19 | { 20 | class var absoluteSystemTime: TimeInterval { 21 | return Double(mach_absolute_time()) * machToSeconds; 22 | } 23 | 24 | @discardableResult class func setRealTimePriority(withPeriod period: TimeInterval) -> Bool 25 | { 26 | var policy = thread_time_constraint_policy() 27 | policy.period = UInt32(period / machToSeconds) 28 | policy.computation = UInt32(0.007 / machToSeconds) 29 | policy.constraint = UInt32(0.03 / machToSeconds) 30 | policy.preemptible = 0 31 | 32 | let threadport = pthread_mach_thread_np(pthread_self()) 33 | let count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) 34 | 35 | var result = KERN_SUCCESS 36 | 37 | withUnsafePointer(to: &policy) { (pointer) in 38 | pointer.withMemoryRebound(to: integer_t.self, capacity: 1) { (policyPointer) in 39 | let mutablePolicyPointer = UnsafeMutablePointer(mutating: policyPointer) 40 | result = thread_policy_set(threadport, UInt32(THREAD_TIME_CONSTRAINT_POLICY), mutablePolicyPointer, count) 41 | } 42 | } 43 | 44 | if result != KERN_SUCCESS 45 | { 46 | print("Thread.setRealTimePriority(withPeriod:) failed.") 47 | return false 48 | } 49 | 50 | return true 51 | } 52 | 53 | class func realTimeWait(until targetTime: TimeInterval) 54 | { 55 | mach_wait_until(UInt64(targetTime / machToSeconds)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/CGGeometry+Conveniences.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGGeometry+Conveniences.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 12/19/15. 6 | // Copyright © 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | internal extension CGRect 12 | { 13 | init?(dictionary: [String: CGFloat]) 14 | { 15 | guard 16 | let x = dictionary["x"], 17 | let y = dictionary["y"], 18 | let width = dictionary["width"], 19 | let height = dictionary["height"] 20 | else { return nil } 21 | 22 | self = CGRect(x: x, y: y, width: width, height: height) 23 | } 24 | 25 | func rounded() -> CGRect 26 | { 27 | var frame = self 28 | frame.origin.x.round() 29 | frame.origin.y.round() 30 | frame.size.width.round() 31 | frame.size.height.round() 32 | 33 | return frame 34 | } 35 | 36 | func scaled(to containingFrame: CGRect) -> CGRect 37 | { 38 | var frame = self.applying(.init(scaleX: containingFrame.width, y: containingFrame.height)) 39 | frame.origin.x += containingFrame.minX 40 | frame.origin.y += containingFrame.minY 41 | 42 | return frame 43 | } 44 | } 45 | 46 | internal extension CGSize 47 | { 48 | init?(dictionary: [String: CGFloat]) 49 | { 50 | guard 51 | let width = dictionary["width"], 52 | let height = dictionary["height"] 53 | else { return nil } 54 | 55 | self = CGSize(width: width, height: height) 56 | } 57 | } 58 | 59 | internal extension UIEdgeInsets 60 | { 61 | init?(dictionary: [String: CGFloat]) 62 | { 63 | let top = dictionary["top"] 64 | let bottom = dictionary["bottom"] 65 | let left = dictionary["left"] 66 | let right = dictionary["right"] 67 | 68 | // Make sure it contains at least one valid value. 69 | guard top != nil || bottom != nil || left != nil || right != nil else { return nil } 70 | 71 | self = UIEdgeInsets(top: top ?? 0, left: left ?? 0, bottom: bottom ?? 0, right: right ?? 0) 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Audio/DLTAMuteSwitchMonitor.m: -------------------------------------------------------------------------------- 1 | // 2 | // DLTAMuteSwitchMonitor.m 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 11/19/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | #import "DLTAMuteSwitchMonitor.h" 10 | 11 | #import 12 | 13 | @import AudioToolbox; 14 | 15 | @interface DLTAMuteSwitchMonitor () 16 | 17 | @property (nonatomic, readwrite) BOOL isMonitoring; 18 | @property (nonatomic, readwrite) BOOL isMuted; 19 | 20 | @property (nonatomic) int notifyToken; 21 | 22 | @end 23 | 24 | @implementation DLTAMuteSwitchMonitor 25 | 26 | - (instancetype)init 27 | { 28 | self = [super init]; 29 | if (self) 30 | { 31 | _isMuted = YES; 32 | } 33 | 34 | return self; 35 | } 36 | 37 | - (void)startMonitoring:(void (^)(BOOL isMuted))muteHandler 38 | { 39 | if ([self isMonitoring]) 40 | { 41 | return; 42 | } 43 | 44 | self.isMonitoring = YES; 45 | 46 | __weak __typeof(self) weakSelf = self; 47 | void (^updateMutedState)(void) = ^{ 48 | if (weakSelf == nil) 49 | { 50 | return; 51 | } 52 | 53 | uint64_t state; 54 | uint32_t result = notify_get_state(weakSelf.notifyToken, &state); 55 | if (result == NOTIFY_STATUS_OK) 56 | { 57 | weakSelf.isMuted = (state == 0); 58 | muteHandler(weakSelf.isMuted); 59 | } 60 | else 61 | { 62 | NSLog(@"Failed to get mute state. Error: %@", @(result)); 63 | } 64 | }; 65 | 66 | NSString *privateAPIName = [[@[@"com", @"apple", @"springboard", @"ring3rstat3"] componentsJoinedByString:@"."] stringByReplacingOccurrencesOfString:@"3" withString:@"e"]; 67 | notify_register_dispatch(privateAPIName.UTF8String, &_notifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(int token) { 68 | updateMutedState(); 69 | }); 70 | 71 | updateMutedState(); 72 | } 73 | 74 | - (void)stopMonitoring 75 | { 76 | if (![self isMonitoring]) 77 | { 78 | return; 79 | } 80 | 81 | self.isMonitoring = NO; 82 | 83 | notify_cancel(self.notifyToken); 84 | } 85 | 86 | @end 87 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DeltaCore", 8 | platforms: [ 9 | .iOS(.v14) 10 | ], 11 | products: [ 12 | .library(name: "DeltaCore", targets: ["DeltaCore", "CDeltaCore"]), 13 | ], 14 | dependencies: [ 15 | .package(name: "ZIPFoundation", url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMinor(from: "0.9.11")) 16 | ], 17 | targets: [ 18 | .target( 19 | name: "CDeltaCore", 20 | dependencies: [], 21 | path: "DeltaCore", 22 | exclude: [ 23 | "Delta.swift", 24 | "Cores", 25 | "Emulator Core/EmulatorCore.swift", 26 | "Emulator Core/Video", 27 | "Emulator Core/Audio/AudioManager.swift", 28 | "Emulator Core/Audio/RingBuffer.swift", 29 | "Extensions", 30 | "Filters", 31 | "Game Controllers", 32 | "Model", 33 | "Protocols", 34 | "Supporting Files", 35 | "Types/ExtensibleEnums.swift", 36 | "UI" 37 | ], 38 | sources: [ 39 | "DeltaTypes.m", 40 | "Emulator Core/Audio/DLTAMuteSwitchMonitor.m", 41 | ], 42 | publicHeadersPath: "include" 43 | ), 44 | .target( 45 | name: "DeltaCore", 46 | dependencies: ["CDeltaCore", "ZIPFoundation"], 47 | path: "DeltaCore", 48 | exclude: [ 49 | "DeltaTypes.m", 50 | "Emulator Core/Audio/DLTAMuteSwitchMonitor.m", 51 | "Supporting Files/Info.plist", 52 | ], 53 | resources: [ 54 | .copy("Supporting Files/KeyboardGameController.deltamapping"), 55 | .copy("Supporting Files/MFiGameController.deltamapping"), 56 | ], 57 | cSettings: [ 58 | .define("GLES_SILENCE_DEPRECATION"), 59 | .define("CI_SILENCE_GL_DEPRECATION") 60 | ] 61 | ), 62 | ] 63 | ) 64 | -------------------------------------------------------------------------------- /DeltaCore/Model/GameControllerInputMapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameControllerInputMapping.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/22/17. 6 | // Copyright © 2017 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct GameControllerInputMapping: GameControllerInputMappingProtocol, Codable 12 | { 13 | public var name: String? 14 | public var gameControllerInputType: GameControllerInputType 15 | 16 | public var supportedControllerInputs: [Input] { 17 | return self.inputMappings.keys.map { AnyInput(stringValue: $0, intValue: nil, type: .controller(self.gameControllerInputType)) } 18 | } 19 | 20 | private var inputMappings: [String: AnyInput] 21 | 22 | public init(gameControllerInputType: GameControllerInputType) 23 | { 24 | self.gameControllerInputType = gameControllerInputType 25 | 26 | self.inputMappings = [:] 27 | } 28 | 29 | public func input(forControllerInput controllerInput: Input) -> Input? 30 | { 31 | precondition(controllerInput.type == .controller(self.gameControllerInputType), "controllerInput.type must match GameControllerInputMapping.gameControllerInputType") 32 | 33 | let input = self.inputMappings[controllerInput.stringValue] 34 | return input 35 | } 36 | } 37 | 38 | public extension GameControllerInputMapping 39 | { 40 | init(fileURL: URL) throws 41 | { 42 | let data = try Data(contentsOf: fileURL) 43 | 44 | let decoder = PropertyListDecoder() 45 | self = try decoder.decode(GameControllerInputMapping.self, from: data) 46 | } 47 | 48 | func write(to url: URL) throws 49 | { 50 | let encoder = PropertyListEncoder() 51 | 52 | let data = try encoder.encode(self) 53 | try data.write(to: url) 54 | } 55 | } 56 | 57 | public extension GameControllerInputMapping 58 | { 59 | mutating func set(_ input: Input?, forControllerInput controllerInput: Input) 60 | { 61 | precondition(controllerInput.type == .controller(self.gameControllerInputType), "controllerInput.type must match GameControllerInputMapping.gameControllerInputType") 62 | 63 | if let input = input 64 | { 65 | self.inputMappings[controllerInput.stringValue] = AnyInput(input) 66 | } 67 | else 68 | { 69 | self.inputMappings[controllerInput.stringValue] = nil 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DeltaCore/Cores/DeltaCoreProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeltaCoreProtocol.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/29/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | public protocol DeltaCoreProtocol: CustomStringConvertible 12 | { 13 | /* General */ 14 | var name: String { get } 15 | var identifier: String { get } 16 | var version: String? { get } 17 | 18 | var gameType: GameType { get } 19 | var gameSaveFileExtension: String { get } 20 | 21 | // Should be associated type, but Swift type system makes this difficult, so ¯\_(ツ)_/¯ 22 | var gameInputType: Input.Type { get } 23 | 24 | /* Rendering */ 25 | var audioFormat: AVAudioFormat { get } 26 | var videoFormat: VideoFormat { get } 27 | 28 | /* Cheats */ 29 | var supportedCheatFormats: Set { get } 30 | 31 | /* Emulation */ 32 | var emulatorBridge: EmulatorBridging { get } 33 | 34 | var resourceBundle: Bundle { get } 35 | } 36 | 37 | public extension DeltaCoreProtocol 38 | { 39 | var version: String? { 40 | return nil 41 | } 42 | 43 | var resourceBundle: Bundle { 44 | #if FRAMEWORK 45 | let bundle = Bundle(for: type(of: self.emulatorBridge)) 46 | #elseif STATIC_LIBRARY || SWIFT_PACKAGE 47 | let bundle: Bundle 48 | if let bundleURL = Bundle.main.url(forResource: self.name, withExtension: "bundle") 49 | { 50 | bundle = Bundle(url: bundleURL)! 51 | } 52 | else 53 | { 54 | bundle = Bundle(for: type(of: self.emulatorBridge)) 55 | } 56 | #else 57 | let bundle = Bundle.main 58 | #endif 59 | 60 | return bundle 61 | } 62 | 63 | var directoryURL: URL { 64 | let directoryURL = Delta.coresDirectoryURL.appendingPathComponent(self.name, isDirectory: true) 65 | 66 | try? FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil) 67 | 68 | return directoryURL 69 | } 70 | } 71 | 72 | public extension DeltaCoreProtocol 73 | { 74 | var description: String { 75 | let description = "\(self.name) (\(self.identifier))" 76 | return description 77 | } 78 | } 79 | 80 | public func ==(lhs: DeltaCoreProtocol?, rhs: DeltaCoreProtocol?) -> Bool 81 | { 82 | return lhs?.identifier == rhs?.identifier 83 | } 84 | 85 | public func !=(lhs: DeltaCoreProtocol?, rhs: DeltaCoreProtocol?) -> Bool 86 | { 87 | return !(lhs == rhs) 88 | } 89 | 90 | public func ~=(lhs: DeltaCoreProtocol?, rhs: DeltaCoreProtocol?) -> Bool 91 | { 92 | return lhs == rhs 93 | } 94 | -------------------------------------------------------------------------------- /DeltaCore.xcodeproj/project.xcworkspace/xcshareddata/DeltaCore.xcscmblueprint: -------------------------------------------------------------------------------- 1 | { 2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "816D57F18EE93972A535858CB18F29A85BE91A39", 3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { 4 | 5 | }, 6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { 7 | "82DF43EF5CA016D15C41E0AF1AF686E9973475B7" : 0, 8 | "816D57F18EE93972A535858CB18F29A85BE91A39" : 0, 9 | "A1BF268530E217C860B34A81A1DA1B446006BF9E" : 0, 10 | "A92F984637B9EAE95E95252EC6CBD8DE42DB61D1" : 0 11 | }, 12 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "0A5660E3-C22C-4BC7-BD54-AAE7E72240E9", 13 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { 14 | "82DF43EF5CA016D15C41E0AF1AF686E9973475B7" : "DeltaCore\/External\/ZipZap\/", 15 | "816D57F18EE93972A535858CB18F29A85BE91A39" : "DeltaCore\/", 16 | "A1BF268530E217C860B34A81A1DA1B446006BF9E" : "..", 17 | "A92F984637B9EAE95E95252EC6CBD8DE42DB61D1" : "DeltaCore\/External\/TPCircularBuffer\/" 18 | }, 19 | "DVTSourceControlWorkspaceBlueprintNameKey" : "DeltaCore", 20 | "DVTSourceControlWorkspaceBlueprintVersion" : 204, 21 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "DeltaCore.xcodeproj", 22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ 23 | { 24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:rileytestut\/DeltaCore.git", 25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "816D57F18EE93972A535858CB18F29A85BE91A39" 27 | }, 28 | { 29 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:pixelglow\/ZipZap.git", 30 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 31 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "82DF43EF5CA016D15C41E0AF1AF686E9973475B7" 32 | }, 33 | { 34 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:rileytestut\/Delta.git", 35 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 36 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "A1BF268530E217C860B34A81A1DA1B446006BF9E" 37 | }, 38 | { 39 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:michaeltyson\/TPCircularBuffer.git", 40 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", 41 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "A92F984637B9EAE95E95252EC6CBD8DE42DB61D1" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /DeltaCore/Model/Inputs/StandardGameControllerInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StandardGameControllerInput.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/20/17. 6 | // Copyright © 2017 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension GameControllerInputType 12 | { 13 | static let standard = GameControllerInputType("standard") 14 | } 15 | 16 | public enum StandardGameControllerInput: String, Codable 17 | { 18 | case menu 19 | 20 | case up 21 | case down 22 | case left 23 | case right 24 | 25 | case leftThumbstickUp 26 | case leftThumbstickDown 27 | case leftThumbstickLeft 28 | case leftThumbstickRight 29 | 30 | case rightThumbstickUp 31 | case rightThumbstickDown 32 | case rightThumbstickLeft 33 | case rightThumbstickRight 34 | 35 | case a 36 | case b 37 | case x 38 | case y 39 | 40 | case start 41 | case select 42 | 43 | case l1 44 | case l2 45 | case l3 46 | 47 | case r1 48 | case r2 49 | case r3 50 | } 51 | 52 | extension StandardGameControllerInput: Input 53 | { 54 | public var type: InputType { 55 | return .controller(.standard) 56 | } 57 | 58 | public var isContinuous: Bool { 59 | switch self 60 | { 61 | case .leftThumbstickUp, .leftThumbstickDown, .leftThumbstickLeft, .leftThumbstickRight: return true 62 | case .rightThumbstickUp, .rightThumbstickDown, .rightThumbstickLeft, .rightThumbstickRight: return true 63 | default: return false 64 | } 65 | } 66 | } 67 | 68 | public extension StandardGameControllerInput 69 | { 70 | private static var inputMappings = [GameType: GameControllerInputMapping]() 71 | 72 | func input(for gameType: GameType) -> Input? 73 | { 74 | if let inputMapping = StandardGameControllerInput.inputMappings[gameType] 75 | { 76 | let input = inputMapping.input(forControllerInput: self) 77 | return input 78 | } 79 | 80 | guard 81 | let deltaCore = Delta.core(for: gameType), 82 | let fileURL = deltaCore.resourceBundle.url(forResource: "Standard", withExtension: "deltamapping") 83 | else { fatalError("Cannot find Standard.deltamapping for game type \(gameType)") } 84 | 85 | do 86 | { 87 | let inputMapping = try GameControllerInputMapping(fileURL: fileURL) 88 | StandardGameControllerInput.inputMappings[gameType] = inputMapping 89 | 90 | let input = inputMapping.input(forControllerInput: self) 91 | return input 92 | } 93 | catch 94 | { 95 | fatalError(String(describing: error)) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Model/ControllerSkinProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControllerSkinProtocol.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 10/13/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol ControllerSkinProtocol 12 | { 13 | var name: String { get } 14 | var identifier: String { get } 15 | var gameType: GameType { get } 16 | var isDebugModeEnabled: Bool { get } 17 | 18 | func supports(_ traits: ControllerSkin.Traits) -> Bool 19 | 20 | func image(for traits: ControllerSkin.Traits, preferredSize: ControllerSkin.Size) -> UIImage? 21 | func thumbstick(for item: ControllerSkin.Item, traits: ControllerSkin.Traits, preferredSize: ControllerSkin.Size) -> (UIImage, CGSize)? 22 | 23 | func items(for traits: ControllerSkin.Traits) -> [ControllerSkin.Item]? 24 | 25 | func isTranslucent(for traits: ControllerSkin.Traits) -> Bool? 26 | 27 | func gameScreenFrame(for traits: ControllerSkin.Traits) -> CGRect? 28 | func screens(for traits: ControllerSkin.Traits) -> [ControllerSkin.Screen]? 29 | 30 | func aspectRatio(for traits: ControllerSkin.Traits) -> CGSize? 31 | func contentSize(for traits: ControllerSkin.Traits) -> CGSize? 32 | 33 | func menuInsets(for traits: ControllerSkin.Traits) -> UIEdgeInsets? 34 | 35 | func supportedTraits(for traits: ControllerSkin.Traits) -> ControllerSkin.Traits? 36 | } 37 | 38 | public extension ControllerSkinProtocol 39 | { 40 | func supportedTraits(for traits: ControllerSkin.Traits) -> ControllerSkin.Traits? 41 | { 42 | var traits = traits 43 | 44 | while !self.supports(traits) 45 | { 46 | if traits.device == .iphone && traits.displayType == .edgeToEdge 47 | { 48 | traits.displayType = .standard 49 | } 50 | else if traits.device == .ipad 51 | { 52 | traits.device = .iphone 53 | traits.displayType = .edgeToEdge 54 | } 55 | else 56 | { 57 | return nil 58 | } 59 | } 60 | 61 | return traits 62 | } 63 | 64 | func gameScreenFrame(for traits: DeltaCore.ControllerSkin.Traits) -> CGRect? 65 | { 66 | return self.screens(for: traits)?.first?.outputFrame 67 | } 68 | } 69 | 70 | public func ==(lhs: ControllerSkinProtocol?, rhs: ControllerSkinProtocol?) -> Bool 71 | { 72 | return lhs?.identifier == rhs?.identifier 73 | } 74 | 75 | public func !=(lhs: ControllerSkinProtocol?, rhs: ControllerSkinProtocol?) -> Bool 76 | { 77 | return !(lhs == rhs) 78 | } 79 | 80 | public func ~=(pattern: ControllerSkinProtocol?, value: ControllerSkinProtocol?) -> Bool 81 | { 82 | return pattern == value 83 | } 84 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/UIImage+PDF.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+PDF.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 12/21/15. 6 | // Copyright © 2015 Riley Testut. All rights reserved. 7 | // 8 | // Based on Erica Sadun's UIImage+PDFUtility ( https://github.com/erica/useful-things/blob/master/useful%20pack/UIImage%2BPDF/UIImage%2BPDFUtility.m ) 9 | // 10 | 11 | import UIKit 12 | import CoreGraphics 13 | import AVFoundation 14 | 15 | internal extension UIImage 16 | { 17 | class func image(withPDFData data: Data, targetSize: CGSize) -> UIImage? 18 | { 19 | guard targetSize.width > 0 && targetSize.height > 0 else { return nil } 20 | 21 | guard 22 | let dataProvider = CGDataProvider(data: data as CFData), 23 | let document = CGPDFDocument(dataProvider), 24 | let page = document.page(at: 1) 25 | else { return nil } 26 | 27 | let pageFrame = page.getBoxRect(.cropBox) 28 | 29 | var destinationFrame = AVMakeRect(aspectRatio: pageFrame.size, insideRect: CGRect(origin: CGPoint.zero, size: targetSize)) 30 | destinationFrame.origin = CGPoint.zero 31 | 32 | let format = UIGraphicsImageRendererFormat() 33 | format.scale = UIScreen.main.scale 34 | let imageRenderer = UIGraphicsImageRenderer(bounds: destinationFrame, format: format) 35 | 36 | let image = imageRenderer.image { (imageRendererContext) in 37 | 38 | let context = imageRendererContext.cgContext 39 | 40 | // Save state 41 | context.saveGState() 42 | 43 | // Flip coordinate system to match Quartz system 44 | let transform = CGAffineTransform.identity.scaledBy(x: 1.0, y: -1.0).translatedBy(x: 0.0, y: -targetSize.height) 45 | context.concatenate(transform) 46 | 47 | // Calculate rendering frames 48 | destinationFrame = destinationFrame.applying(transform) 49 | 50 | let aspectScale = min(destinationFrame.width / pageFrame.width, destinationFrame.height / pageFrame.height) 51 | 52 | // Ensure aspect ratio is preserved 53 | var drawingFrame = pageFrame.applying(CGAffineTransform(scaleX: aspectScale, y: aspectScale)) 54 | drawingFrame.origin.x = destinationFrame.midX - (drawingFrame.width / 2.0) 55 | drawingFrame.origin.y = destinationFrame.midY - (drawingFrame.height / 2.0) 56 | 57 | // Scale the context 58 | context.translateBy(x: destinationFrame.minX, y: destinationFrame.minY) 59 | context.scaleBy(x: aspectScale, y: aspectScale) 60 | 61 | // Render the PDF 62 | context.drawPDFPage(page) 63 | 64 | // Restore state 65 | context.restoreGState() 66 | } 67 | 68 | return image 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Inputs/Input.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Input.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/4/15. 6 | // Copyright © 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | public enum InputType: Codable 10 | { 11 | case controller(GameControllerInputType) 12 | case game(GameType) 13 | } 14 | 15 | extension InputType: RawRepresentable 16 | { 17 | public var rawValue: String { 18 | switch self 19 | { 20 | case .controller(let inputType): return inputType.rawValue 21 | case .game(let gameType): return gameType.rawValue 22 | } 23 | } 24 | 25 | public init(rawValue: String) 26 | { 27 | let gameType = GameType(rawValue) 28 | 29 | if Delta.core(for: gameType) != nil 30 | { 31 | self = .game(gameType) 32 | } 33 | else 34 | { 35 | let inputType = GameControllerInputType(rawValue) 36 | self = .controller(inputType) 37 | } 38 | } 39 | } 40 | 41 | extension InputType: Hashable 42 | { 43 | public func hash(into hasher: inout Hasher) 44 | { 45 | hasher.combine(self.rawValue) 46 | } 47 | } 48 | 49 | /// "Private" parent protocol allows types to conform at declaration without having 50 | /// to implement the full Input protocol (which might require circular dependencies). 51 | /// 52 | /// Inheriting from CodingKey allows compiler to automatically generate intValue/stringValue logic for enums. 53 | public protocol _Input: CodingKey 54 | { 55 | var stringValue: String { get } 56 | var intValue: Int? { get } 57 | 58 | init?(stringValue: String) 59 | init?(intValue: Int) 60 | } 61 | 62 | public protocol Input: _Input 63 | { 64 | var type: InputType { get } 65 | var isContinuous: Bool { get } 66 | } 67 | 68 | public extension Input where Self: RawRepresentable, RawValue == String 69 | { 70 | var stringValue: String { 71 | return self.rawValue 72 | } 73 | 74 | var intValue: Int? { 75 | return nil 76 | } 77 | 78 | init?(stringValue: String) 79 | { 80 | self.init(rawValue: stringValue) 81 | } 82 | 83 | init?(intValue: Int) 84 | { 85 | return nil 86 | } 87 | } 88 | 89 | public extension Input 90 | { 91 | var isContinuous: Bool { 92 | return false 93 | } 94 | 95 | init?(input: Input) 96 | { 97 | self.init(stringValue: input.stringValue) 98 | 99 | guard self.type == input.type else { return nil } 100 | } 101 | } 102 | 103 | public func ==(lhs: Input?, rhs: Input?) -> Bool 104 | { 105 | return lhs?.type == rhs?.type && lhs?.stringValue == rhs?.stringValue 106 | } 107 | 108 | public func !=(lhs: Input?, rhs: Input?) -> Bool 109 | { 110 | return !(lhs == rhs) 111 | } 112 | 113 | public func ~=(pattern: Input?, value: Input?) -> Bool 114 | { 115 | return pattern == value 116 | } 117 | -------------------------------------------------------------------------------- /DeltaCore/UI/Controller/ControllerInputView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControllerInputView.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/17/18. 6 | // Copyright © 2018 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private var isKeyboardWindowKey: UInt8 = 0 12 | 13 | internal extension UIWindow 14 | { 15 | // Hacky, but allows us to reliably detect keyboard window without private APIs. 16 | var isKeyboardWindow: Bool { 17 | get { objc_getAssociatedObject(self, &isKeyboardWindowKey) as? Bool ?? false } 18 | set { objc_setAssociatedObject(self, &isKeyboardWindowKey, newValue as NSNumber, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY) } 19 | } 20 | } 21 | 22 | class ControllerInputView: UIInputView 23 | { 24 | let controllerView: ControllerView 25 | 26 | private var previousBounds: CGRect? 27 | 28 | private var aspectRatioConstraint: NSLayoutConstraint? { 29 | didSet { 30 | oldValue?.isActive = false 31 | } 32 | } 33 | 34 | init(frame: CGRect) 35 | { 36 | self.controllerView = ControllerView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height)) 37 | self.controllerView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 38 | self.controllerView.isControllerInputView = true 39 | 40 | super.init(frame: frame, inputViewStyle: .keyboard) 41 | 42 | self.addSubview(self.controllerView) 43 | 44 | self.translatesAutoresizingMaskIntoConstraints = false 45 | self.allowsSelfSizing = true 46 | 47 | self.setNeedsUpdateConstraints() 48 | } 49 | 50 | required init?(coder aDecoder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | override func didMoveToWindow() 55 | { 56 | super.didMoveToWindow() 57 | 58 | guard let window = self.window else { return } 59 | window.isKeyboardWindow = true 60 | 61 | if self.bounds.width == window.bounds.width 62 | { 63 | // Keyboard already matches window width, but other traits might have changed so update controller skin just to be safe. 64 | // Limit this to when widths match to avoid duplicate calls to updateControllerSkin() via layoutSubviews(). 65 | self.controllerView.updateControllerSkin() 66 | } 67 | } 68 | 69 | override func layoutSubviews() 70 | { 71 | super.layoutSubviews() 72 | 73 | guard 74 | let controllerSkin = self.controllerView.controllerSkin, 75 | let traits = self.controllerView.controllerSkinTraits, 76 | let aspectRatio = controllerSkin.aspectRatio(for: traits) 77 | else { return } 78 | 79 | if self.bounds != self.previousBounds 80 | { 81 | self.controllerView.updateControllerSkin() 82 | } 83 | 84 | self.previousBounds = self.bounds 85 | 86 | let multiplier = aspectRatio.height / aspectRatio.width 87 | guard self.aspectRatioConstraint?.multiplier != multiplier else { return } 88 | 89 | self.aspectRatioConstraint = self.heightAnchor.constraint(equalTo: self.widthAnchor, multiplier: multiplier) 90 | self.aspectRatioConstraint?.isActive = true 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DeltaCore/UI/Controller/ControllerDebugView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControllerDebugView.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 12/20/15. 6 | // Copyright © 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | 12 | internal class ControllerDebugView: UIView 13 | { 14 | var items: [ControllerSkin.Item]? { 15 | didSet { 16 | self.updateItems() 17 | } 18 | } 19 | 20 | weak var appPlacementLayoutGuide: UILayoutGuide? 21 | 22 | private var itemViews = [ItemView]() 23 | 24 | override init(frame: CGRect) 25 | { 26 | super.init(frame: frame) 27 | 28 | self.initialize() 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) 32 | { 33 | super.init(coder: aDecoder) 34 | 35 | self.initialize() 36 | } 37 | 38 | private func initialize() 39 | { 40 | self.backgroundColor = UIColor.clear 41 | self.isUserInteractionEnabled = false 42 | } 43 | 44 | override func layoutSubviews() 45 | { 46 | super.layoutSubviews() 47 | 48 | for view in self.itemViews 49 | { 50 | var containingFrame = self.bounds 51 | if let layoutGuide = self.appPlacementLayoutGuide, view.item.placement == .app 52 | { 53 | containingFrame = layoutGuide.layoutFrame 54 | } 55 | 56 | let frame = view.item.extendedFrame.scaled(to: containingFrame) 57 | view.frame = frame 58 | } 59 | } 60 | 61 | private func updateItems() 62 | { 63 | self.itemViews.forEach { $0.removeFromSuperview() } 64 | 65 | var itemViews = [ItemView]() 66 | 67 | for item in (self.items ?? []) 68 | { 69 | let itemView = ItemView(item: item) 70 | self.addSubview(itemView) 71 | 72 | itemViews.append(itemView) 73 | } 74 | 75 | self.itemViews = itemViews 76 | 77 | self.setNeedsLayout() 78 | } 79 | } 80 | 81 | private class ItemView: UIView 82 | { 83 | let item: ControllerSkin.Item 84 | 85 | private let label: UILabel 86 | 87 | init(item: ControllerSkin.Item) 88 | { 89 | self.item = item 90 | 91 | self.label = UILabel() 92 | self.label.translatesAutoresizingMaskIntoConstraints = false 93 | self.label.textColor = UIColor.white 94 | self.label.font = UIFont.boldSystemFont(ofSize: 16) 95 | 96 | var text = "" 97 | 98 | for input in item.inputs.allInputs 99 | { 100 | if text.isEmpty 101 | { 102 | text = input.stringValue 103 | } 104 | else 105 | { 106 | text = text + "," + input.stringValue 107 | } 108 | } 109 | 110 | self.label.text = text 111 | 112 | self.label.sizeToFit() 113 | 114 | super.init(frame: CGRect.zero) 115 | 116 | self.addSubview(self.label) 117 | 118 | self.label.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true 119 | self.label.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true 120 | 121 | self.backgroundColor = UIColor.red.withAlphaComponent(0.75) 122 | } 123 | 124 | required init?(coder aDecoder: NSCoder) 125 | { 126 | fatalError() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /DeltaCore/Model/Inputs/AnyInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyInput.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/24/17. 6 | // Copyright © 2017 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct AnyInput: Input, Codable, Hashable 12 | { 13 | public let stringValue: String 14 | public let intValue: Int? 15 | 16 | public var type: InputType 17 | public var isContinuous: Bool 18 | 19 | public init(_ input: Input) 20 | { 21 | self.init(stringValue: input.stringValue, intValue: input.intValue, type: input.type, isContinuous: input.isContinuous) 22 | } 23 | 24 | public init(stringValue: String, intValue: Int?, type: InputType, isContinuous: Bool? = nil) 25 | { 26 | self.stringValue = stringValue 27 | self.intValue = intValue 28 | 29 | self.type = type 30 | self.isContinuous = false 31 | 32 | if let isContinuous = isContinuous 33 | { 34 | self.isContinuous = isContinuous 35 | } 36 | else 37 | { 38 | switch type 39 | { 40 | case .game(let gameType): 41 | guard let deltaCore = Delta.core(for: gameType), let input = deltaCore.gameInputType.init(stringValue: self.stringValue) else { break } 42 | self.isContinuous = input.isContinuous 43 | 44 | case .controller(.standard): 45 | guard let standardInput = StandardGameControllerInput(input: self) else { break } 46 | self.isContinuous = standardInput.isContinuous 47 | 48 | case .controller(.mfi): 49 | guard let mfiInput = MFiGameController.Input(input: self) else { break } 50 | self.isContinuous = mfiInput.isContinuous 51 | 52 | case .controller: 53 | // FIXME: We have no way to look up arbitrary controller inputs at runtime, so just leave isContinuous as false for now. 54 | // In practice this is not too bad, since it's very uncommon to map from an input to a non-standard controller input. 55 | break 56 | } 57 | } 58 | } 59 | } 60 | 61 | public extension AnyInput 62 | { 63 | init?(stringValue: String) 64 | { 65 | return nil 66 | } 67 | 68 | init?(intValue: Int) 69 | { 70 | return nil 71 | } 72 | } 73 | 74 | public extension AnyInput 75 | { 76 | private enum CodingKeys: String, CodingKey 77 | { 78 | case stringValue = "identifier" 79 | case type 80 | } 81 | 82 | init(from decoder: Decoder) throws 83 | { 84 | let container = try decoder.container(keyedBy: CodingKeys.self) 85 | 86 | let stringValue = try container.decode(String.self, forKey: .stringValue) 87 | let type = try container.decode(InputType.self, forKey: .type) 88 | 89 | let intValue: Int? 90 | 91 | switch type 92 | { 93 | case .controller: intValue = nil 94 | case .game(let gameType): 95 | guard let deltaCore = Delta.core(for: gameType), let input = deltaCore.gameInputType.init(stringValue: stringValue) else { 96 | throw DecodingError.dataCorruptedError(forKey: .stringValue, in: container, debugDescription: "The Input game type \(gameType) is unsupported.") 97 | } 98 | 99 | intValue = input.intValue 100 | } 101 | 102 | self.init(stringValue: stringValue, intValue: intValue, type: type) 103 | } 104 | 105 | func encode(to encoder: Encoder) throws 106 | { 107 | var container = encoder.container(keyedBy: CodingKeys.self) 108 | try container.encode(self.stringValue, forKey: .stringValue) 109 | try container.encode(self.type, forKey: .type) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /DeltaCore/Protocols/Inputs/GameController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameController.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 5/3/15. 6 | // Copyright (c) 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | import ObjectiveC 10 | 11 | private var gameControllerStateManagerKey = 0 12 | 13 | //MARK: - GameControllerReceiver - 14 | public protocol GameControllerReceiver: AnyObject 15 | { 16 | /// Equivalent to pressing a button, or moving an analog stick 17 | func gameController(_ gameController: GameController, didActivate input: Input, value: Double) 18 | 19 | /// Equivalent to releasing a button or an analog stick 20 | func gameController(_ gameController: GameController, didDeactivate input: Input) 21 | } 22 | 23 | //MARK: - GameController - 24 | public protocol GameController: NSObjectProtocol 25 | { 26 | var name: String { get } 27 | 28 | var playerIndex: Int? { get set } 29 | 30 | var inputType: GameControllerInputType { get } 31 | 32 | var defaultInputMapping: GameControllerInputMappingProtocol? { get } 33 | } 34 | 35 | public extension GameController 36 | { 37 | private var stateManager: GameControllerStateManager { 38 | var stateManager = objc_getAssociatedObject(self, &gameControllerStateManagerKey) as? GameControllerStateManager 39 | 40 | if stateManager == nil 41 | { 42 | stateManager = GameControllerStateManager(gameController: self) 43 | objc_setAssociatedObject(self, &gameControllerStateManagerKey, stateManager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 44 | } 45 | 46 | return stateManager! 47 | } 48 | 49 | var receivers: [GameControllerReceiver] { 50 | return self.stateManager.receivers 51 | } 52 | 53 | var activatedInputs: [AnyInput: Double] { 54 | return self.stateManager.activatedInputs 55 | } 56 | 57 | var sustainedInputs: [AnyInput: Double] { 58 | return self.stateManager.sustainedInputs 59 | } 60 | } 61 | 62 | public extension GameController 63 | { 64 | func addReceiver(_ receiver: GameControllerReceiver) 65 | { 66 | self.addReceiver(receiver, inputMapping: self.defaultInputMapping) 67 | } 68 | 69 | func addReceiver(_ receiver: GameControllerReceiver, inputMapping: GameControllerInputMappingProtocol?) 70 | { 71 | self.stateManager.addReceiver(receiver, inputMapping: inputMapping) 72 | } 73 | 74 | func removeReceiver(_ receiver: GameControllerReceiver) 75 | { 76 | self.stateManager.removeReceiver(receiver) 77 | } 78 | 79 | func activate(_ input: Input, value: Double = 1.0) 80 | { 81 | self.stateManager.activate(input, value: value) 82 | } 83 | 84 | func deactivate(_ input: Input) 85 | { 86 | self.stateManager.deactivate(input) 87 | } 88 | 89 | func sustain(_ input: Input, value: Double = 1.0) 90 | { 91 | self.stateManager.sustain(input, value: value) 92 | } 93 | 94 | func unsustain(_ input: Input) 95 | { 96 | self.stateManager.unsustain(input) 97 | } 98 | } 99 | 100 | public extension GameController 101 | { 102 | func inputMapping(for receiver: GameControllerReceiver) -> GameControllerInputMappingProtocol? 103 | { 104 | return self.stateManager.inputMapping(for: receiver) 105 | } 106 | 107 | func mappedInput(for input: Input, receiver: GameControllerReceiver) -> Input? 108 | { 109 | return self.stateManager.mappedInput(for: input, receiver: receiver) 110 | } 111 | } 112 | 113 | public func ==(lhs: GameController?, rhs: GameController?) -> Bool 114 | { 115 | switch (lhs, rhs) 116 | { 117 | case (nil, nil): return true 118 | case (_?, nil): return false 119 | case (nil, _?): return false 120 | case (let lhs?, let rhs?): return lhs.isEqual(rhs) 121 | } 122 | } 123 | 124 | public func !=(lhs: GameController?, rhs: GameController?) -> Bool 125 | { 126 | return !(lhs == rhs) 127 | } 128 | 129 | public func ~=(pattern: GameController?, value: GameController?) -> Bool 130 | { 131 | return pattern == value 132 | } 133 | -------------------------------------------------------------------------------- /DeltaCore/Model/CheatFormat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheatFormat.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 5/22/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct CheatFormat: Hashable 12 | { 13 | public let name: String 14 | 15 | // Must begin and end with an alphanumberic character. Besides these, non-alphanumberic characters will be treated as special formatting characters. 16 | // Ex: XXXX-YYYY. The "-" is a special formatting character, and should be automatically inserted between alphanumeric characters by a code editor 17 | public let format: String 18 | 19 | public let type: CheatType 20 | 21 | public let allowedCodeCharacters: CharacterSet 22 | 23 | public init(name: String, format: String, type: CheatType, allowedCodeCharacters: CharacterSet = CharacterSet.hexadecimals) 24 | { 25 | self.name = name 26 | self.format = format 27 | self.type = type 28 | self.allowedCodeCharacters = allowedCodeCharacters 29 | } 30 | } 31 | 32 | public extension String 33 | { 34 | func sanitized(with characterSet: CharacterSet) -> String 35 | { 36 | let sanitizedString = (self as NSString).components(separatedBy: characterSet.inverted).joined(separator: "") 37 | return sanitizedString 38 | } 39 | 40 | func formatted(with cheatFormat: CheatFormat) -> String 41 | { 42 | // NOTE: We do not use cheatFormat.allowedCodeCharacters because the code format typically includes non-legal characters. 43 | // Ex: Using "XXXX-YYYY" for the code format despite the actual code format being strictly hexadecimal characters. 44 | // This is okay because this function's job is not to validate the input, but simply to format it 45 | let characterSet = CharacterSet.alphanumerics 46 | 47 | // Remove all characters not in characterSet 48 | let sanitizedFormat = cheatFormat.format.sanitized(with: characterSet) 49 | 50 | // We need to repeat the format enough times so it is greater than or equal to the length of self 51 | // This prevents us from having to account for wrapping around the cheat format 52 | let repetitions = Int(ceil((Float(self.count) / Float(sanitizedFormat.count)))) 53 | 54 | var format = "" 55 | for i in 0 ..< repetitions 56 | { 57 | if i > 0 58 | { 59 | format += "\n" 60 | } 61 | 62 | format += cheatFormat.format 63 | } 64 | 65 | 66 | var formattedString = "" 67 | 68 | // Serves as a sort of stack buffer for us to draw characters from 69 | let codeBuffer = NSMutableString(string: self) 70 | 71 | let scanner = Scanner(string: format) 72 | scanner.charactersToBeSkipped = nil 73 | 74 | while !scanner.isAtEnd 75 | { 76 | // Scan up until the first separator character 77 | let string = scanner.scanCharacters(from: characterSet) 78 | 79 | // Might start with separator characters, in which case scannedString would be nil/empty 80 | if let scannedString = string, !scannedString.isEmpty 81 | { 82 | let range = NSRange(location: 0, length: min(scannedString.count, codeBuffer.length)) 83 | 84 | // "Pop off" characters from the front of codeBuffer 85 | let code = codeBuffer.substring(with: range) 86 | codeBuffer.replaceCharacters(in: range, with: "") 87 | 88 | formattedString += code 89 | 90 | // No characters left in buffer means we've finished formatting 91 | guard codeBuffer.length > 0 else { break } 92 | } 93 | 94 | // Scan all separator characters. If no separator characters, we're done! 95 | guard let separatorString = scanner.scanUpToCharacters(from: characterSet), !separatorString.isEmpty else { break } 96 | formattedString += separatorString 97 | } 98 | 99 | // Ensure it is all uppercase 100 | return formattedString.uppercased() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Video/BitmapProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BitmapProcessor.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 4/8/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import CoreImage 10 | import Accelerate 11 | 12 | fileprivate extension VideoFormat.PixelFormat 13 | { 14 | var nativeCIFormat: CIFormat? { 15 | switch self 16 | { 17 | case .rgb565: return nil 18 | case .bgra8: return .BGRA8 19 | case .rgba8: return .RGBA8 20 | } 21 | } 22 | } 23 | 24 | fileprivate extension VideoFormat 25 | { 26 | var pixelFormat: PixelFormat { 27 | switch self.format 28 | { 29 | case .bitmap(let format): return format 30 | case .openGLES2, .openGLES3: fatalError("Should not be using VideoFormat.Format.openGLES with BitmapProcessor.") 31 | } 32 | } 33 | 34 | var bufferSize: Int { 35 | let bufferSize = Int(self.dimensions.width * self.dimensions.height) * self.pixelFormat.bytesPerPixel 36 | return bufferSize 37 | } 38 | } 39 | 40 | class BitmapProcessor: VideoProcessor 41 | { 42 | let videoFormat: VideoFormat 43 | let videoBuffer: UnsafeMutablePointer? 44 | 45 | var viewport: CGRect = .zero 46 | 47 | private let outputVideoFormat: VideoFormat 48 | private let outputVideoBuffer: UnsafeMutablePointer 49 | 50 | init(videoFormat: VideoFormat) 51 | { 52 | self.videoFormat = videoFormat 53 | 54 | switch self.videoFormat.pixelFormat 55 | { 56 | case .rgb565: self.outputVideoFormat = VideoFormat(format: .bitmap(.bgra8), dimensions: self.videoFormat.dimensions) 57 | case .bgra8, .rgba8: self.outputVideoFormat = self.videoFormat 58 | } 59 | 60 | self.videoBuffer = UnsafeMutablePointer.allocate(capacity: self.videoFormat.bufferSize) 61 | self.outputVideoBuffer = UnsafeMutablePointer.allocate(capacity: self.outputVideoFormat.bufferSize) 62 | } 63 | 64 | deinit 65 | { 66 | self.videoBuffer?.deallocate() 67 | self.outputVideoBuffer.deallocate() 68 | } 69 | } 70 | 71 | extension BitmapProcessor 72 | { 73 | func prepare() 74 | { 75 | } 76 | 77 | func processFrame() -> CIImage? 78 | { 79 | guard let ciFormat = self.outputVideoFormat.pixelFormat.nativeCIFormat else { 80 | print("VideoManager output format is not supported.") 81 | return nil 82 | } 83 | 84 | return autoreleasepool { 85 | var inputVImageBuffer = vImage_Buffer(data: self.videoBuffer, height: vImagePixelCount(self.videoFormat.dimensions.height), width: vImagePixelCount(self.videoFormat.dimensions.width), rowBytes: self.videoFormat.pixelFormat.bytesPerPixel * Int(self.videoFormat.dimensions.width)) 86 | var outputVImageBuffer = vImage_Buffer(data: self.outputVideoBuffer, height: vImagePixelCount(self.outputVideoFormat.dimensions.height), width: vImagePixelCount(self.outputVideoFormat.dimensions.width), rowBytes: self.outputVideoFormat.pixelFormat.bytesPerPixel * Int(self.outputVideoFormat.dimensions.width)) 87 | 88 | switch self.videoFormat.pixelFormat 89 | { 90 | case .rgb565: vImageConvert_RGB565toBGRA8888(255, &inputVImageBuffer, &outputVImageBuffer, 0) 91 | case .bgra8, .rgba8: 92 | // Ensure alpha value is 255, not 0. 93 | // 0x1 refers to the Blue channel in ARGB, which corresponds to the Alpha channel in BGRA and RGBA. 94 | vImageOverwriteChannelsWithScalar_ARGB8888(255, &inputVImageBuffer, &outputVImageBuffer, 0x1, vImage_Flags(kvImageNoFlags)) 95 | } 96 | 97 | let bitmapData = Data(bytes: self.outputVideoBuffer, count: self.outputVideoFormat.bufferSize) 98 | 99 | var image = CIImage(bitmapData: bitmapData, bytesPerRow: self.outputVideoFormat.pixelFormat.bytesPerPixel * Int(self.outputVideoFormat.dimensions.width), size: self.outputVideoFormat.dimensions, format: ciFormat, colorSpace: nil) 100 | 101 | if let viewport = self.correctedViewport 102 | { 103 | image = image.cropped(to: viewport) 104 | } 105 | 106 | return image 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /DeltaCore/Model/ControllerSkinTraits.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControllerSkinTraits.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/4/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension ControllerSkin 12 | { 13 | public enum Device: String, CaseIterable 14 | { 15 | // Naming conventions? I treat the "P" as the capital letter, so since it's a value (not a type) I've opted to lowercase it 16 | case iphone 17 | case ipad 18 | case tv 19 | } 20 | 21 | public enum DisplayType: String, CaseIterable 22 | { 23 | case standard 24 | case edgeToEdge 25 | case splitView 26 | } 27 | 28 | public enum Orientation: String, CaseIterable 29 | { 30 | case portrait 31 | case landscape 32 | } 33 | 34 | public enum Size: String 35 | { 36 | case small 37 | case medium 38 | case large 39 | } 40 | 41 | public struct Traits: Hashable, CustomStringConvertible 42 | { 43 | public var device: Device 44 | public var displayType: DisplayType 45 | public var orientation: Orientation 46 | 47 | /// CustomStringConvertible 48 | public var description: String { 49 | return self.device.rawValue + "-" + self.displayType.rawValue + "-" + self.orientation.rawValue 50 | } 51 | 52 | public init(device: Device, displayType: DisplayType, orientation: Orientation) 53 | { 54 | self.device = device 55 | self.displayType = displayType 56 | self.orientation = orientation 57 | } 58 | 59 | public static func defaults(for window: UIWindow) -> ControllerSkin.Traits 60 | { 61 | let device: Device 62 | let displayType: DisplayType 63 | let orientation: Orientation 64 | 65 | if let scene = window.windowScene, scene.session.role == .windowExternalDisplay 66 | { 67 | //TODO: Support .portrait TV skins 68 | device = .tv 69 | displayType = .standard 70 | orientation = .landscape 71 | } 72 | else 73 | { 74 | // Use trait collection to determine device because our container app may be containing us in an "iPhone" trait collection despite being on iPad 75 | // 99% of the time, won't make a difference ¯\_(ツ)_/¯ 76 | if window.traitCollection.userInterfaceIdiom == .pad 77 | { 78 | device = .ipad 79 | 80 | if window.isKeyboardWindow 81 | { 82 | displayType = .splitView 83 | 84 | // Screen bounds may be incorrect when entering background, but keyboard window bounds is full screen and always accurate, so use window bounds instead. 85 | orientation = (window.bounds.width > window.bounds.height) ? .landscape : .portrait 86 | } 87 | else if !window.bounds.equalTo(window.screen.bounds) 88 | { 89 | displayType = .splitView 90 | 91 | // Use screen bounds because in split view window bounds might be portrait, but device is actually landscape (and we want landscape skin). 92 | orientation = (window.screen.bounds.width > window.screen.bounds.height) ? .landscape : .portrait 93 | } 94 | else 95 | { 96 | displayType = .standard 97 | orientation = (window.bounds.width > window.bounds.height) ? .landscape : .portrait 98 | } 99 | } 100 | else 101 | { 102 | device = .iphone 103 | displayType = (window.safeAreaInsets.bottom != 0) ? .edgeToEdge : .standard 104 | orientation = (window.bounds.width > window.bounds.height) ? .landscape : .portrait 105 | } 106 | } 107 | 108 | let traits = ControllerSkin.Traits(device: device, displayType: displayType, orientation: orientation) 109 | return traits 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /DeltaCore/Supporting Files/MFiGameController.deltamapping: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gameControllerInputType 6 | mfi 7 | inputMappings 8 | 9 | a 10 | 11 | identifier 12 | a 13 | type 14 | standard 15 | 16 | b 17 | 18 | identifier 19 | b 20 | type 21 | standard 22 | 23 | down 24 | 25 | identifier 26 | down 27 | type 28 | standard 29 | 30 | left 31 | 32 | identifier 33 | left 34 | type 35 | standard 36 | 37 | leftShoulder 38 | 39 | identifier 40 | l1 41 | type 42 | standard 43 | 44 | leftThumbstickDown 45 | 46 | identifier 47 | leftThumbstickDown 48 | type 49 | standard 50 | 51 | leftThumbstickLeft 52 | 53 | identifier 54 | leftThumbstickLeft 55 | type 56 | standard 57 | 58 | leftThumbstickRight 59 | 60 | identifier 61 | leftThumbstickRight 62 | type 63 | standard 64 | 65 | leftThumbstickUp 66 | 67 | identifier 68 | leftThumbstickUp 69 | type 70 | standard 71 | 72 | leftTrigger 73 | 74 | identifier 75 | l2 76 | type 77 | standard 78 | 79 | menu 80 | 81 | identifier 82 | menu 83 | type 84 | standard 85 | 86 | right 87 | 88 | identifier 89 | right 90 | type 91 | standard 92 | 93 | rightShoulder 94 | 95 | identifier 96 | r1 97 | type 98 | standard 99 | 100 | rightThumbstickDown 101 | 102 | identifier 103 | rightThumbstickDown 104 | type 105 | standard 106 | 107 | rightThumbstickLeft 108 | 109 | identifier 110 | rightThumbstickLeft 111 | type 112 | standard 113 | 114 | rightThumbstickRight 115 | 116 | identifier 117 | rightThumbstickRight 118 | type 119 | standard 120 | 121 | rightThumbstickUp 122 | 123 | identifier 124 | rightThumbstickUp 125 | type 126 | standard 127 | 128 | rightTrigger 129 | 130 | identifier 131 | r2 132 | type 133 | standard 134 | 135 | select 136 | 137 | identifier 138 | select 139 | type 140 | standard 141 | 142 | start 143 | 144 | identifier 145 | start 146 | type 147 | standard 148 | 149 | up 150 | 151 | identifier 152 | up 153 | type 154 | standard 155 | 156 | x 157 | 158 | identifier 159 | x 160 | type 161 | standard 162 | 163 | y 164 | 165 | identifier 166 | y 167 | type 168 | standard 169 | 170 | 171 | name 172 | Default MFi 173 | 174 | 175 | -------------------------------------------------------------------------------- /DeltaCore/Extensions/UIScene+KeyboardFocus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScene+KeyboardFocus.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/19/22. 6 | // Copyright © 2022 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private var isTrackingKeyboardFocusKey: UInt8 = 0 12 | private var keyboardFocusTimerKey: UInt8 = 0 13 | 14 | @objc private protocol UIScenePrivate: NSObjectProtocol 15 | { 16 | var _isTargetOfKeyboardEventDeferringEnvironment: Bool { get } 17 | } 18 | 19 | @available(iOS 13, *) 20 | extension UIScene 21 | { 22 | public static let keyboardFocusDidChangeNotification: Notification.Name = .init("keyboardFocusDidChangeNotification") 23 | 24 | public var hasKeyboardFocus: Bool { 25 | guard self.responds(to: #selector(getter: UIScenePrivate._isTargetOfKeyboardEventDeferringEnvironment)) else { 26 | // Default to true, or else emulation will never resume due to thinking we don't have keyboard focus. 27 | return true 28 | } 29 | 30 | guard !ProcessInfo.processInfo.isiOSAppOnMac && !ProcessInfo.processInfo.isRunningOnVisionPro else { 31 | // scene._isTargetOfKeyboardEventDeferringEnvironment always returns false when running on macOS and visionOS, 32 | // so return true instead to ensure everything continues working. 33 | return true 34 | } 35 | 36 | let scene = unsafeBitCast(self, to: UIScenePrivate.self) 37 | let hasKeyboardFocus = scene._isTargetOfKeyboardEventDeferringEnvironment 38 | return hasKeyboardFocus 39 | } 40 | 41 | private var isTrackingKeyboardFocus: Bool { 42 | get { 43 | let numberValue = objc_getAssociatedObject(self, &isTrackingKeyboardFocusKey) as? NSNumber 44 | return numberValue?.boolValue ?? false 45 | } 46 | set { 47 | let numberValue = newValue ? NSNumber(value: newValue) : nil 48 | objc_setAssociatedObject(self, &isTrackingKeyboardFocusKey, numberValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) 49 | } 50 | } 51 | 52 | private var keyboardFocusTimer: Timer? { 53 | get { objc_getAssociatedObject(self, &keyboardFocusTimerKey) as? Timer } 54 | set { objc_setAssociatedObject(self, &keyboardFocusTimerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 55 | } 56 | 57 | func startTrackingKeyboardFocus() 58 | { 59 | guard !self.isTrackingKeyboardFocus else { return } 60 | self.isTrackingKeyboardFocus = true 61 | 62 | NotificationCenter.default.addObserver(self, selector: #selector(UIScene.didReceiveKeyboardFocus(_:)), name: Notification.Name("_UISceneDidBecomeTargetOfKeyboardEventDeferringEnvironmentNotification"), object: self) 63 | NotificationCenter.default.addObserver(self, selector: #selector(UIScene.didLoseKeyboardFocus(_:)), name: Notification.Name("_UISceneDidResignTargetOfKeyboardEventDeferringEnvironmentNotification"), object: self) 64 | } 65 | } 66 | 67 | @objc @available(iOS 13, *) 68 | private extension UIScene 69 | { 70 | func didReceiveKeyboardFocus(_ notification: Notification) 71 | { 72 | guard self.activationState == .foregroundActive else { return } 73 | 74 | // Ignore false positives when switching foreground applications. 75 | self.keyboardFocusTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] timer in 76 | guard let self = self, timer.isValid, self.hasKeyboardFocus else { return } 77 | NotificationCenter.default.post(name: UIScene.keyboardFocusDidChangeNotification, object: self) 78 | } 79 | } 80 | 81 | func didLoseKeyboardFocus(_ notification: Notification) 82 | { 83 | if #available(iOS 16, *), let windowScene = self as? UIWindowScene, windowScene.isStageManagerEnabled 84 | { 85 | // Stage Manager is enabled, so listen for all keyboard change notifications. 86 | } 87 | else 88 | { 89 | // Stage Manager is not enabled, so ignore keyboard change notifications unless we're active in foreground. 90 | guard self.activationState == .foregroundActive else { return } 91 | } 92 | 93 | if let timer = self.keyboardFocusTimer, timer.isValid 94 | { 95 | self.keyboardFocusTimer?.invalidate() 96 | self.keyboardFocusTimer = nil 97 | } 98 | else 99 | { 100 | NotificationCenter.default.post(name: UIScene.keyboardFocusDidChangeNotification, object: self) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /DeltaCore/UI/Controller/TouchControllerSkin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TouchControllerSkin.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 12/1/20. 6 | // Copyright © 2020 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | extension TouchControllerSkin 13 | { 14 | public enum LayoutAxis: String, CaseIterable 15 | { 16 | case vertical 17 | case horizontal 18 | } 19 | } 20 | 21 | public struct TouchControllerSkin 22 | { 23 | public var name: String { "TouchControllerSkin" } 24 | public var identifier: String { "com.delta.TouchControllerSkin" } 25 | public var gameType: GameType { self.controllerSkin.gameType } 26 | public var isDebugModeEnabled: Bool { false } 27 | 28 | public var screenLayoutAxis: LayoutAxis = .vertical 29 | public var screenPredicate: ((ControllerSkin.Screen) -> Bool)? 30 | 31 | private let controllerSkin: ControllerSkinProtocol 32 | 33 | public init(controllerSkin: ControllerSkinProtocol) 34 | { 35 | self.controllerSkin = controllerSkin 36 | } 37 | } 38 | 39 | extension TouchControllerSkin: ControllerSkinProtocol 40 | { 41 | public func supports(_ traits: ControllerSkin.Traits) -> Bool 42 | { 43 | return true 44 | } 45 | 46 | public func image(for traits: ControllerSkin.Traits, preferredSize: ControllerSkin.Size) -> UIImage? 47 | { 48 | return nil 49 | } 50 | 51 | public func thumbstick(for item: ControllerSkin.Item, traits: ControllerSkin.Traits, preferredSize: ControllerSkin.Size) -> (UIImage, CGSize)? 52 | { 53 | return nil 54 | } 55 | 56 | public func items(for traits: ControllerSkin.Traits) -> [ControllerSkin.Item]? 57 | { 58 | guard 59 | var touchScreenItem = self.controllerSkin.items(for: traits)?.first(where: { $0.kind == .touchScreen }), 60 | let screens = self.screens(for: traits), let touchScreen = screens.first(where: { $0.isTouchScreen }), 61 | let outputFrame = touchScreen.outputFrame 62 | else { return nil } 63 | 64 | // For now, we assume touchScreenItem completely covers the touch screen. 65 | touchScreenItem.placement = .app 66 | touchScreenItem.frame = outputFrame 67 | touchScreenItem.extendedFrame = outputFrame 68 | return [touchScreenItem] 69 | } 70 | 71 | public func isTranslucent(for traits: ControllerSkin.Traits) -> Bool? 72 | { 73 | return false 74 | } 75 | 76 | public func screens(for traits: ControllerSkin.Traits) -> [ControllerSkin.Screen]? 77 | { 78 | guard let screens = self.controllerSkin.screens(for: traits) else { return nil } 79 | 80 | // Filter screens first so we can use filteredScreens.count in calculations. 81 | let filteredScreens = screens.filter(self.screenPredicate ?? { _ in true }) 82 | 83 | let updatedScreens = filteredScreens.enumerated().map { (index, screen) -> ControllerSkin.Screen in 84 | let length = 1.0 / CGFloat(filteredScreens.count) 85 | 86 | var screen = screen 87 | screen.placement = .app 88 | 89 | switch self.screenLayoutAxis 90 | { 91 | case .horizontal: screen.outputFrame = CGRect(x: length * CGFloat(index), y: 0, width: length, height: 1.0) 92 | case .vertical: screen.outputFrame = CGRect(x: 0, y: length * CGFloat(index), width: 1.0, height: length) 93 | } 94 | 95 | return screen 96 | } 97 | 98 | return updatedScreens 99 | } 100 | 101 | public func aspectRatio(for traits: ControllerSkin.Traits) -> CGSize? 102 | { 103 | return self.controllerSkin.aspectRatio(for: traits) 104 | } 105 | 106 | public func contentSize(for traits: ControllerSkin.Traits) -> CGSize? 107 | { 108 | guard let screens = self.screens(for: traits) else { return nil } 109 | 110 | let compositeScreenSize = screens.reduce(into: CGSize.zero) { (size, screen) in 111 | guard let inputFrame = screen.inputFrame else { return } 112 | 113 | switch self.screenLayoutAxis 114 | { 115 | case .horizontal: 116 | size.width += inputFrame.width 117 | size.height = max(inputFrame.height, size.height) 118 | 119 | case .vertical: 120 | size.width = max(inputFrame.width, size.width) 121 | size.height += inputFrame.height 122 | } 123 | } 124 | 125 | return compositeScreenSize 126 | } 127 | 128 | public func menuInsets(for traits: ControllerSkin.Traits) -> UIEdgeInsets? 129 | { 130 | return nil 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /DeltaCore/Filters/FilterChain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterChain.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 4/13/17. 6 | // Copyright © 2017 Riley Testut. All rights reserved. 7 | // 8 | 9 | import CoreImage 10 | 11 | private extension CIImage 12 | { 13 | func flippingYAxis() -> CIImage 14 | { 15 | let transform = CGAffineTransform(scaleX: 1, y: -1) 16 | let flippedImage = self.applyingFilter("CIAffineTransform", parameters: ["inputTransform": NSValue(cgAffineTransform: transform)]) 17 | 18 | let translation = CGAffineTransform(translationX: 0, y: self.extent.height) 19 | let translatedImage = flippedImage.applyingFilter("CIAffineTransform", parameters: ["inputTransform": NSValue(cgAffineTransform: translation)]) 20 | 21 | return translatedImage 22 | } 23 | } 24 | 25 | private extension CIFilter 26 | { 27 | struct Values: Hashable 28 | { 29 | var name: String 30 | var inputValues: NSDictionary 31 | 32 | static func ==(lhs: Values, rhs: Values) -> Bool 33 | { 34 | return lhs.name == rhs.name && lhs.inputValues.isEqual(rhs.inputValues) 35 | } 36 | 37 | func hash(into hasher: inout Hasher) 38 | { 39 | hasher.combine(self.name) 40 | hasher.combine(self.inputValues) 41 | } 42 | } 43 | 44 | func values() -> Values 45 | { 46 | let inputValues = self.inputKeys.compactMap { (key) -> (String, Any)? in 47 | // Ignore inputImage value, since that shouldn't affect CIFilter equality. 48 | // Filters with same inputKeys + values should always be equal, even with different inputImages. 49 | guard let value = self.value(forKey: key), key != kCIInputImageKey else { return nil } 50 | return (key, value) 51 | } 52 | 53 | let dictionary = inputValues.reduce(into: [:]) { $0[$1.0] = $1.1 } 54 | 55 | let values = Values(name: self.name, inputValues: dictionary as NSDictionary) 56 | return values 57 | } 58 | } 59 | 60 | @objcMembers 61 | public class FilterChain: CIFilter 62 | { 63 | public var inputFilters = [CIFilter]() 64 | 65 | public var inputImage: CIImage? 66 | 67 | public override var outputImage: CIImage? { 68 | return self.inputFilters.reduce(self.inputImage, { (image, filter) -> CIImage? in 69 | guard let image = image else { return nil } 70 | 71 | let flippedImage = image.flippingYAxis() 72 | 73 | var outputImage: CIImage? 74 | 75 | if filter.inputKeys.contains(kCIInputImageKey) 76 | { 77 | filter.setValue(flippedImage, forKey: kCIInputImageKey) 78 | outputImage = filter.outputImage 79 | } 80 | else 81 | { 82 | guard var filterImage = filter.outputImage else { return image } 83 | 84 | if filterImage.extent.isInfinite 85 | { 86 | filterImage = filterImage.cropped(to: flippedImage.extent) 87 | } 88 | 89 | // Filter is already "flipped", so no need to flip it again. 90 | // filterImage = filterImage.flippingYAxis() 91 | 92 | outputImage = filterImage.composited(over: flippedImage) 93 | } 94 | 95 | outputImage = outputImage?.flippingYAxis() 96 | 97 | if let image = outputImage, image.extent.origin != .zero 98 | { 99 | // Always translate CIImage back to origin so later calculations are correct. 100 | let translation = CGAffineTransform(translationX: -image.extent.origin.x, y: -image.extent.origin.y) 101 | outputImage = image.applyingFilter("CIAffineTransform", parameters: ["inputTransform": NSValue(cgAffineTransform: translation)]) 102 | } 103 | 104 | return outputImage 105 | }) 106 | } 107 | 108 | public override var hash: Int { 109 | return self.inputFiltersValues.hashValue 110 | } 111 | 112 | private var inputFiltersValues: [CIFilter.Values] { 113 | let values = self.inputFilters.map { $0.values() } 114 | return values 115 | } 116 | 117 | public override init() 118 | { 119 | // Must be declared or else we'll get "Use of unimplemented initializer FilterChain.init()" runtime exception. 120 | super.init() 121 | } 122 | 123 | public init(filters: [CIFilter]) 124 | { 125 | self.inputFilters = filters 126 | super.init() 127 | } 128 | 129 | public required init?(coder aDecoder: NSCoder) 130 | { 131 | super.init(coder: aDecoder) 132 | } 133 | 134 | public override func isEqual(_ object: Any?) -> Bool 135 | { 136 | guard let filterChain = object as? FilterChain else { return false } 137 | 138 | let isEqual = self.inputFiltersValues == filterChain.inputFiltersValues 139 | return isEqual 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /DeltaCore/Model/GameControllerStateManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameControllerStateManager.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 5/29/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal class GameControllerStateManager 12 | { 13 | let gameController: GameController 14 | 15 | private(set) var activatedInputs = [AnyInput: Double]() 16 | private(set) var sustainedInputs = [AnyInput: Double]() 17 | 18 | var receivers: [GameControllerReceiver] { 19 | var objects: [GameControllerReceiver]! 20 | 21 | self.dispatchQueue.sync { 22 | objects = self._receivers.keyEnumerator().allObjects as? [GameControllerReceiver] 23 | } 24 | 25 | return objects 26 | } 27 | 28 | private let _receivers = NSMapTable.weakToStrongObjects() 29 | 30 | // Used to synchronize access to _receivers to prevent race conditions (yay ObjC) 31 | private let dispatchQueue = DispatchQueue(label: "com.rileytestut.Delta.GameControllerStateManager.dispatchQueue") 32 | 33 | 34 | init(gameController: GameController) 35 | { 36 | self.gameController = gameController 37 | } 38 | } 39 | 40 | extension GameControllerStateManager 41 | { 42 | func addReceiver(_ receiver: GameControllerReceiver, inputMapping: GameControllerInputMappingProtocol?) 43 | { 44 | self.dispatchQueue.sync { 45 | self._receivers.setObject(inputMapping as AnyObject, forKey: receiver) 46 | } 47 | } 48 | 49 | func removeReceiver(_ receiver: GameControllerReceiver) 50 | { 51 | self.dispatchQueue.sync { 52 | self._receivers.removeObject(forKey: receiver) 53 | } 54 | } 55 | } 56 | 57 | extension GameControllerStateManager 58 | { 59 | func activate(_ input: Input, value: Double) 60 | { 61 | precondition(input.type == .controller(self.gameController.inputType), "input.type must match self.gameController.inputType") 62 | 63 | // An input may be "activated" multiple times, such as by pressing different buttons that map to same input, or moving an analog stick. 64 | self.activatedInputs[AnyInput(input)] = value 65 | 66 | for receiver in self.receivers 67 | { 68 | if let mappedInput = self.mappedInput(for: input, receiver: receiver) 69 | { 70 | receiver.gameController(self.gameController, didActivate: mappedInput, value: value) 71 | } 72 | } 73 | } 74 | 75 | func deactivate(_ input: Input) 76 | { 77 | precondition(input.type == .controller(self.gameController.inputType), "input.type must match self.gameController.inputType") 78 | 79 | // Unlike activate(_:), we don't allow an input to be deactivated multiple times. 80 | guard self.activatedInputs.keys.contains(AnyInput(input)) else { return } 81 | 82 | if let sustainedValue = self.sustainedInputs[AnyInput(input)] 83 | { 84 | if input.isContinuous 85 | { 86 | // Input is continuous and currently sustained, so reset value to sustained value. 87 | self.activate(input, value: sustainedValue) 88 | } 89 | } 90 | else 91 | { 92 | // Not sustained, so simply deactivate it. 93 | self.activatedInputs[AnyInput(input)] = nil 94 | 95 | for receiver in self.receivers 96 | { 97 | if let mappedInput = self.mappedInput(for: input, receiver: receiver) 98 | { 99 | let hasActivatedMappedControllerInputs = self.activatedInputs.keys.contains { 100 | guard let input = self.mappedInput(for: $0, receiver: receiver) else { return false } 101 | return input == mappedInput 102 | } 103 | 104 | if !hasActivatedMappedControllerInputs 105 | { 106 | // All controller inputs that map to this input have been deactivated, so we can deactivate the mapped input. 107 | receiver.gameController(self.gameController, didDeactivate: mappedInput) 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | func sustain(_ input: Input, value: Double) 115 | { 116 | precondition(input.type == .controller(self.gameController.inputType), "input.type must match self.gameController.inputType") 117 | 118 | if self.activatedInputs[AnyInput(input)] != value 119 | { 120 | self.activate(input, value: value) 121 | } 122 | 123 | self.sustainedInputs[AnyInput(input)] = value 124 | } 125 | 126 | // Technically not a word, but no good alternative, so ¯\_(ツ)_/¯ 127 | func unsustain(_ input: Input) 128 | { 129 | precondition(input.type == .controller(self.gameController.inputType), "input.type must match self.gameController.inputType") 130 | 131 | self.sustainedInputs[AnyInput(input)] = nil 132 | 133 | self.deactivate(AnyInput(input)) 134 | } 135 | } 136 | 137 | extension GameControllerStateManager 138 | { 139 | func inputMapping(for receiver: GameControllerReceiver) -> GameControllerInputMappingProtocol? 140 | { 141 | let inputMapping = self._receivers.object(forKey: receiver) as? GameControllerInputMappingProtocol 142 | return inputMapping 143 | } 144 | 145 | func mappedInput(for input: Input, receiver: GameControllerReceiver) -> Input? 146 | { 147 | guard let inputMapping = self.inputMapping(for: receiver) else { return input } 148 | 149 | let mappedInput = inputMapping.input(forControllerInput: input) 150 | return mappedInput 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Audio/RingBuffer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RingBuffer.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/29/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | // Heavily based on Michael Tyson's TPCircularBuffer (https://github.com/michaeltyson/TPCircularBuffer) 9 | // 10 | 11 | import Foundation 12 | import Darwin.Mach.machine.vm_types 13 | 14 | private func trunc_page(_ x: vm_size_t) -> vm_size_t 15 | { 16 | return x & ~(vm_page_size - 1) 17 | } 18 | 19 | private func round_page(_ x: vm_size_t) -> vm_size_t 20 | { 21 | return trunc_page(x + (vm_size_t(vm_page_size) - 1)) 22 | } 23 | 24 | @objc(DLTARingBuffer) @objcMembers 25 | public class RingBuffer: NSObject 26 | { 27 | public var isEnabled: Bool = true 28 | 29 | public var availableBytesForWriting: Int { 30 | return Int(self.bufferLength - Int(self.usedBytesCount)) 31 | } 32 | 33 | public var availableBytesForReading: Int { 34 | return Int(self.usedBytesCount) 35 | } 36 | 37 | private var head: UnsafeMutableRawPointer { 38 | let head = self.buffer.advanced(by: self.headOffset) 39 | return head 40 | } 41 | 42 | private var tail: UnsafeMutableRawPointer { 43 | let head = self.buffer.advanced(by: self.tailOffset) 44 | return head 45 | } 46 | 47 | private let buffer: UnsafeMutableRawPointer 48 | private var bufferLength = 0 49 | private var tailOffset = 0 50 | private var headOffset = 0 51 | private var usedBytesCount: Int32 = 0 52 | 53 | public init?(preferredBufferSize: Int) 54 | { 55 | assert(preferredBufferSize > 0) 56 | 57 | // To handle race conditions, repeat initialization process up to 3 times before failing. 58 | for _ in 1...3 59 | { 60 | let length = round_page(vm_size_t(preferredBufferSize)) 61 | self.bufferLength = Int(length) 62 | 63 | var bufferAddress: vm_address_t = 0 64 | guard vm_allocate(mach_task_self_, &bufferAddress, vm_size_t(length * 2), VM_FLAGS_ANYWHERE) == ERR_SUCCESS else { continue } 65 | 66 | guard vm_deallocate(mach_task_self_, bufferAddress + length, length) == ERR_SUCCESS else { 67 | vm_deallocate(mach_task_self_, bufferAddress, length) 68 | continue 69 | } 70 | 71 | var virtualAddress: vm_address_t = bufferAddress + length 72 | var current_protection: vm_prot_t = 0 73 | var max_protection: vm_prot_t = 0 74 | 75 | guard vm_remap(mach_task_self_, &virtualAddress, length, 0, 0, mach_task_self_, bufferAddress, 0, ¤t_protection, &max_protection, VM_INHERIT_DEFAULT) == ERR_SUCCESS else { 76 | vm_deallocate(mach_task_self_, bufferAddress, length) 77 | continue 78 | } 79 | 80 | guard virtualAddress == bufferAddress + length else { 81 | vm_deallocate(mach_task_self_, virtualAddress, length) 82 | vm_deallocate(mach_task_self_, bufferAddress, length) 83 | 84 | continue 85 | } 86 | 87 | self.buffer = UnsafeMutableRawPointer(bitPattern: UInt(bufferAddress))! 88 | 89 | return 90 | } 91 | 92 | return nil 93 | } 94 | 95 | deinit 96 | { 97 | let address = UInt(bitPattern: self.buffer) 98 | vm_deallocate(mach_task_self_, vm_address_t(address), vm_size_t(self.bufferLength * 2)) 99 | } 100 | } 101 | 102 | public extension RingBuffer 103 | { 104 | /// Writes `size` bytes from `buffer` to ring buffer if possible. Otherwise, writes as many as possible. 105 | @objc(writeBuffer:size:) 106 | @discardableResult func write(_ buffer: UnsafeRawPointer, size: Int) -> Int 107 | { 108 | guard self.isEnabled else { return 0 } 109 | guard self.availableBytesForWriting > 0 else { return 0 } 110 | 111 | if size > self.availableBytesForWriting 112 | { 113 | print("Ring Buffer Capacity reached. Available: \(self.availableBytesForWriting). Requested: \(size) Max: \(self.bufferLength). Filled: \(self.usedBytesCount).") 114 | 115 | self.reset() 116 | } 117 | 118 | let size = min(size, self.availableBytesForWriting) 119 | memcpy(self.head, buffer, size) 120 | 121 | self.decrementAvailableBytes(by: size) 122 | 123 | return size 124 | } 125 | 126 | /// Copies `size` bytes from ring buffer to `buffer` if possible. Otherwise, copies as many as possible. 127 | @objc(readIntoBuffer:preferredSize:) 128 | @discardableResult func read(into buffer: UnsafeMutableRawPointer, preferredSize: Int) -> Int 129 | { 130 | guard self.isEnabled else { return 0 } 131 | guard self.availableBytesForReading > 0 else { return 0 } 132 | 133 | if preferredSize > self.availableBytesForReading 134 | { 135 | print("Ring Buffer Empty. Available: \(self.availableBytesForReading). Requested: \(preferredSize) Max: \(self.bufferLength). Filled: \(self.usedBytesCount).") 136 | 137 | self.reset() 138 | } 139 | 140 | let size = min(preferredSize, self.availableBytesForReading) 141 | memcpy(buffer, self.tail, size) 142 | 143 | self.incrementAvailableBytes(by: size) 144 | 145 | return size 146 | } 147 | 148 | func reset() 149 | { 150 | let size = self.availableBytesForReading 151 | self.incrementAvailableBytes(by: size) 152 | } 153 | } 154 | 155 | private extension RingBuffer 156 | { 157 | func incrementAvailableBytes(by size: Int) 158 | { 159 | self.tailOffset = (self.tailOffset + size) % self.bufferLength 160 | OSAtomicAdd32(-Int32(size), &self.usedBytesCount) 161 | } 162 | 163 | func decrementAvailableBytes(by size: Int) 164 | { 165 | self.headOffset = (self.headOffset + size) % self.bufferLength 166 | OSAtomicAdd32(Int32(size), &self.usedBytesCount) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Video/OpenGLESProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenGLESProcessor.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 4/8/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import CoreImage 10 | import GLKit 11 | 12 | class OpenGLESProcessor: VideoProcessor 13 | { 14 | var videoFormat: VideoFormat { 15 | didSet { 16 | self.resizeVideoBuffers() 17 | } 18 | } 19 | 20 | var viewport: CGRect = .zero { 21 | didSet { 22 | self.resizeVideoBuffers() 23 | } 24 | } 25 | 26 | private let context: EAGLContext 27 | 28 | private var framebuffer: GLuint = 0 29 | private var texture: GLuint = 0 30 | private var renderbuffer: GLuint = 0 31 | 32 | private var indexBuffer: GLuint = 0 33 | private var vertexBuffer: GLuint = 0 34 | 35 | init(videoFormat: VideoFormat, context: EAGLContext) 36 | { 37 | self.videoFormat = videoFormat 38 | 39 | switch videoFormat.format 40 | { 41 | case .openGLES2: self.context = EAGLContext(api: .openGLES2, sharegroup: context.sharegroup)! 42 | case .openGLES3: self.context = EAGLContext(api: .openGLES3, sharegroup: context.sharegroup)! 43 | case .bitmap: fatalError("VideoFormat.Format.bitmap is not supported with OpenGLESProcessor.") 44 | } 45 | } 46 | 47 | deinit 48 | { 49 | if self.renderbuffer > 0 50 | { 51 | glDeleteRenderbuffers(1, &self.renderbuffer) 52 | } 53 | 54 | if self.texture > 0 55 | { 56 | glDeleteTextures(1, &self.texture) 57 | } 58 | 59 | if self.framebuffer > 0 60 | { 61 | glDeleteFramebuffers(1, &self.framebuffer) 62 | } 63 | 64 | if self.indexBuffer > 0 65 | { 66 | glDeleteBuffers(1, &self.indexBuffer) 67 | } 68 | 69 | if self.vertexBuffer > 0 70 | { 71 | glDeleteBuffers(1, &self.vertexBuffer) 72 | } 73 | } 74 | } 75 | 76 | extension OpenGLESProcessor 77 | { 78 | var videoBuffer: UnsafeMutablePointer? { 79 | return nil 80 | } 81 | 82 | func prepare() 83 | { 84 | struct Vertex 85 | { 86 | var x: GLfloat 87 | var y: GLfloat 88 | var z: GLfloat 89 | 90 | var u: GLfloat 91 | var v: GLfloat 92 | } 93 | 94 | EAGLContext.setCurrent(self.context) 95 | 96 | // Vertex buffer 97 | let vertices = [Vertex(x: -1.0, y: -1.0, z: 1.0, u: 0.0, v: 0.0), 98 | Vertex(x: 1.0, y: -1.0, z: 1.0, u: 1.0, v: 0.0), 99 | Vertex(x: 1.0, y: 1.0, z: 1.0, u: 1.0, v: 1.0), 100 | Vertex(x: -1.0, y: 1.0, z: 1.0, u: 0.0, v: 1.0)] 101 | glGenBuffers(1, &self.vertexBuffer) 102 | glBindBuffer(GLenum(GL_ARRAY_BUFFER), self.vertexBuffer) 103 | glBufferData(GLenum(GL_ARRAY_BUFFER), MemoryLayout.size * vertices.count, vertices, GLenum(GL_DYNAMIC_DRAW)) 104 | glBindBuffer(GLenum(GL_ARRAY_BUFFER), 0) 105 | 106 | // Index buffer 107 | let indices: [GLushort] = [0, 1, 2, 0, 2, 3] 108 | glGenBuffers(1, &self.indexBuffer) 109 | glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), self.indexBuffer) 110 | glBufferData(GLenum(GL_ELEMENT_ARRAY_BUFFER), MemoryLayout.size * indices.count, indices, GLenum(GL_STATIC_DRAW)) 111 | glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), 0) 112 | 113 | // Framebuffer 114 | glGenFramebuffers(1, &self.framebuffer) 115 | glBindFramebuffer(GLenum(GL_FRAMEBUFFER), self.framebuffer) 116 | 117 | // Texture 118 | glGenTextures(1, &self.texture) 119 | glBindTexture(GLenum(GL_TEXTURE_2D), self.texture) 120 | glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GLint(GL_LINEAR)) 121 | glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GLint(GL_LINEAR)) 122 | glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GLint(GL_CLAMP_TO_EDGE)) 123 | glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GLint(GL_CLAMP_TO_EDGE)) 124 | glBindTexture(GLenum(GL_TEXTURE_2D), 0) 125 | glFramebufferTexture2D(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_TEXTURE_2D), self.texture, 0) 126 | 127 | // Renderbuffer 128 | glGenRenderbuffers(1, &self.renderbuffer) 129 | glBindRenderbuffer(GLenum(GL_RENDERBUFFER), self.renderbuffer) 130 | glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_DEPTH_ATTACHMENT), GLenum(GL_RENDERBUFFER), self.renderbuffer) 131 | 132 | self.resizeVideoBuffers() 133 | } 134 | 135 | func processFrame() -> CIImage? 136 | { 137 | glFlush() 138 | 139 | var image = CIImage(texture: self.texture, size: self.videoFormat.dimensions, flipped: false, colorSpace: nil) 140 | 141 | if let viewport = self.correctedViewport 142 | { 143 | image = image.cropped(to: viewport) 144 | } 145 | 146 | return image 147 | } 148 | } 149 | 150 | private extension OpenGLESProcessor 151 | { 152 | func resizeVideoBuffers() 153 | { 154 | guard self.texture > 0 && self.renderbuffer > 0 else { return } 155 | 156 | EAGLContext.setCurrent(self.context) 157 | 158 | glBindTexture(GLenum(GL_TEXTURE_2D), self.texture) 159 | glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(self.videoFormat.dimensions.width), GLsizei(self.videoFormat.dimensions.height), 0, GLenum(GL_RGBA), GLenum(GL_UNSIGNED_BYTE), nil) 160 | glBindTexture(GLenum(GL_TEXTURE_2D), 0) 161 | 162 | glBindRenderbuffer(GLenum(GL_RENDERBUFFER), self.renderbuffer) 163 | glRenderbufferStorage(GLenum(GL_RENDERBUFFER), GLenum(GL_DEPTH_COMPONENT16), GLsizei(self.videoFormat.dimensions.width), GLsizei(self.videoFormat.dimensions.height)) 164 | 165 | var viewport = CGRect(origin: .zero, size: self.videoFormat.dimensions) 166 | if let correctedViewport = self.correctedViewport 167 | { 168 | viewport = correctedViewport 169 | } 170 | 171 | glViewport(GLsizei(viewport.minX), GLsizei(viewport.minY), GLsizei(viewport.width), GLsizei(viewport.height)) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /DeltaCore/Game Controllers/Keyboard/KeyboardResponder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardResponder.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/14/18. 6 | // Copyright © 2018 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ObjectiveC 11 | 12 | public extension UIResponder 13 | { 14 | @objc func keyPressesBegan(_ presses: Set, with event: UIEvent) 15 | { 16 | self.next?.keyPressesBegan(presses, with: event) 17 | } 18 | 19 | @objc func keyPressesEnded(_ presses: Set, with event: UIEvent) 20 | { 21 | self.next?.keyPressesEnded(presses, with: event) 22 | } 23 | } 24 | 25 | extension UIResponder 26 | { 27 | @objc(_keyCommandForEvent:target:) 28 | @NSManaged func _keyCommand(for event: UIEvent, target: UnsafeMutablePointer) -> UIKeyCommand? 29 | } 30 | 31 | @objc public class KeyPress: NSObject 32 | { 33 | public fileprivate(set) var key: String 34 | public fileprivate(set) var keyCode: Int 35 | 36 | public fileprivate(set) var modifierFlags: UIKeyModifierFlags 37 | 38 | public fileprivate(set) var isActive: Bool = true 39 | 40 | fileprivate init(key: String, keyCode: Int, modifierFlags: UIKeyModifierFlags) 41 | { 42 | self.key = key 43 | self.keyCode = keyCode 44 | self.modifierFlags = modifierFlags 45 | } 46 | } 47 | 48 | public class KeyboardResponder: UIResponder 49 | { 50 | private let _nextResponder: UIResponder? 51 | 52 | public override var next: UIResponder? { 53 | return self._nextResponder 54 | } 55 | 56 | // Use KeyPress.keyCode as dictionary key because KeyPress.key may be invalid for keyUp events. 57 | private static var activeKeyPresses = [Int: KeyPress]() 58 | private static var activeModifierFlags = UIKeyModifierFlags(rawValue: 0) 59 | 60 | public init(nextResponder: UIResponder?) 61 | { 62 | self._nextResponder = nextResponder 63 | } 64 | } 65 | 66 | extension KeyboardResponder 67 | { 68 | // Implementation based on Steve Troughton-Smith's gist: https://gist.github.com/steventroughtonsmith/7515380 69 | internal override func _keyCommand(for event: UIEvent, target: UnsafeMutablePointer) -> UIKeyCommand? 70 | { 71 | // Retrieve information from event. 72 | guard 73 | let key = event.value(forKey: "_unmodifiedInput") as? String, 74 | let keyCode = event.value(forKey: "_keyCode") as? Int, 75 | let rawModifierFlags = event.value(forKey: "_modifierFlags") as? Int, 76 | let isActive = event.value(forKey: "_isKeyDown") as? Bool 77 | else { return nil } 78 | 79 | let modifierFlags = UIKeyModifierFlags(rawValue: rawModifierFlags) 80 | defer { KeyboardResponder.activeModifierFlags = modifierFlags } 81 | 82 | let previousKeyPress = KeyboardResponder.activeKeyPresses[keyCode] 83 | 84 | // Ignore key presses that haven't changed activate state to filter out duplicate key press events. 85 | guard previousKeyPress?.isActive != isActive else { return nil } 86 | 87 | // Attempt to use previousKeyPress.key because key may be invalid for keyUp events. 88 | var pressedKey = previousKeyPress?.key ?? key 89 | 90 | // Check if pressedKey is a whitespace or newline character. 91 | if pressedKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 92 | { 93 | if pressedKey.isEmpty 94 | { 95 | if isActive 96 | { 97 | // Determine the newly activated modifier key. 98 | let activatedModifierFlags = modifierFlags.subtracting(KeyboardResponder.activeModifierFlags) 99 | 100 | guard let key = self.key(for: activatedModifierFlags) else { return nil } 101 | pressedKey = key 102 | } 103 | else 104 | { 105 | // Determine the newly deactivated modifier key. 106 | let deactivatedModifierFlags = KeyboardResponder.activeModifierFlags.subtracting(modifierFlags) 107 | 108 | guard let key = self.key(for: deactivatedModifierFlags) else { return nil } 109 | pressedKey = key 110 | } 111 | } 112 | else 113 | { 114 | switch pressedKey 115 | { 116 | case " ": pressedKey = KeyboardGameController.Input.space.rawValue 117 | case "\r", "\n": pressedKey = KeyboardGameController.Input.return.rawValue 118 | case "\t": pressedKey = KeyboardGameController.Input.tab.rawValue 119 | default: break 120 | } 121 | } 122 | } 123 | else 124 | { 125 | switch pressedKey 126 | { 127 | case UIKeyCommand.inputUpArrow: pressedKey = KeyboardGameController.Input.up.rawValue 128 | case UIKeyCommand.inputDownArrow: pressedKey = KeyboardGameController.Input.down.rawValue 129 | case UIKeyCommand.inputLeftArrow: pressedKey = KeyboardGameController.Input.left.rawValue 130 | case UIKeyCommand.inputRightArrow: pressedKey = KeyboardGameController.Input.right.rawValue 131 | case UIKeyCommand.inputEscape: pressedKey = KeyboardGameController.Input.escape.rawValue 132 | default: break 133 | } 134 | } 135 | 136 | let keyPress = previousKeyPress ?? KeyPress(key: pressedKey, keyCode: keyCode, modifierFlags: modifierFlags) 137 | keyPress.isActive = isActive 138 | 139 | if keyPress.isActive 140 | { 141 | KeyboardResponder.activeKeyPresses[keyCode] = keyPress 142 | 143 | UIResponder.firstResponder?.keyPressesBegan([keyPress], with: event) 144 | ExternalGameControllerManager.shared.keyPressesBegan([keyPress], with: event) 145 | } 146 | else 147 | { 148 | UIResponder.firstResponder?.keyPressesEnded([keyPress], with: event) 149 | ExternalGameControllerManager.shared.keyPressesEnded([keyPress], with: event) 150 | 151 | KeyboardResponder.activeKeyPresses[keyCode] = nil 152 | } 153 | 154 | return nil 155 | } 156 | 157 | private func key(for modifierFlags: UIKeyModifierFlags) -> String? 158 | { 159 | switch modifierFlags 160 | { 161 | case .shift: return KeyboardGameController.Input.shift.rawValue 162 | case .control: return KeyboardGameController.Input.control.rawValue 163 | case .alternate: return KeyboardGameController.Input.option.rawValue 164 | case .command: return KeyboardGameController.Input.command.rawValue 165 | case .alphaShift: return KeyboardGameController.Input.capsLock.rawValue 166 | default: return nil 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /DeltaCore/Game Controllers/Keyboard/KeyboardGameController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardGameController.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 6/14/18. 6 | // Copyright © 2018 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import GameController 11 | 12 | public extension GameControllerInputType 13 | { 14 | static let keyboard = GameControllerInputType("keyboard") 15 | } 16 | 17 | extension KeyboardGameController 18 | { 19 | public struct Input: Hashable, RawRepresentable, Codable 20 | { 21 | public let rawValue: String 22 | 23 | public init(rawValue: String) 24 | { 25 | self.rawValue = rawValue 26 | } 27 | 28 | public init(_ rawValue: String) 29 | { 30 | self.rawValue = rawValue 31 | } 32 | } 33 | } 34 | 35 | extension KeyboardGameController.Input: Input 36 | { 37 | public var type: InputType { 38 | return .controller(.keyboard) 39 | } 40 | 41 | public init(stringValue: String) 42 | { 43 | self.init(rawValue: stringValue) 44 | } 45 | } 46 | 47 | public extension KeyboardGameController.Input 48 | { 49 | static let up = KeyboardGameController.Input("up") 50 | static let down = KeyboardGameController.Input("down") 51 | static let left = KeyboardGameController.Input("left") 52 | static let right = KeyboardGameController.Input("right") 53 | 54 | static let escape = KeyboardGameController.Input("escape") 55 | 56 | static let shift = KeyboardGameController.Input("shift") 57 | static let command = KeyboardGameController.Input("command") 58 | static let option = KeyboardGameController.Input("option") 59 | static let control = KeyboardGameController.Input("control") 60 | static let capsLock = KeyboardGameController.Input("capsLock") 61 | 62 | static let space = KeyboardGameController.Input("space") 63 | static let `return` = KeyboardGameController.Input("return") 64 | static let tab = KeyboardGameController.Input("tab") 65 | } 66 | 67 | public class KeyboardGameController: UIResponder, GameController 68 | { 69 | public var name: String { 70 | return NSLocalizedString("Keyboard", comment: "") 71 | } 72 | 73 | public var playerIndex: Int? 74 | 75 | public let inputType: GameControllerInputType = .keyboard 76 | 77 | public private(set) lazy var defaultInputMapping: GameControllerInputMappingProtocol? = { 78 | guard let fileURL = Bundle.resources.url(forResource: "KeyboardGameController", withExtension: "deltamapping") else { 79 | fatalError("KeyboardGameController.deltamapping does not exist.") 80 | } 81 | 82 | do 83 | { 84 | let inputMapping = try GameControllerInputMapping(fileURL: fileURL) 85 | return inputMapping 86 | } 87 | catch 88 | { 89 | print(error) 90 | 91 | fatalError("KeyboardGameController.deltamapping does not exist.") 92 | } 93 | }() 94 | 95 | // When non-nil, uses modern keyboard handling. 96 | private let keyboard: GCKeyboard? 97 | 98 | public init(keyboard: GCKeyboard?) 99 | { 100 | self.keyboard = keyboard 101 | 102 | super.init() 103 | 104 | self.keyboard?.keyboardInput?.keyChangedHandler = { [weak self] (profile, buttonInput, keyCode, isActive) in 105 | let input: Input 106 | 107 | switch keyCode 108 | { 109 | case .upArrow: input = .up 110 | case .downArrow: input = .down 111 | case .leftArrow: input = .left 112 | case .rightArrow: input = .right 113 | 114 | case .escape: input = .escape 115 | 116 | case .leftShift, .rightShift: input = .shift 117 | case .leftGUI, .rightGUI: input = .command 118 | case .leftAlt, .rightAlt: input = .option 119 | case .leftControl, .rightControl: input = .control 120 | case .capsLock: input = .capsLock 121 | 122 | case .spacebar: input = .space 123 | case .returnOrEnter, .keypadEnter: input = .return 124 | case .tab: input = .tab 125 | 126 | case .comma: input = .init(",") 127 | case .period, .keypadPeriod: input = .init(".") 128 | case .slash, .keypadSlash: input = .init("/") 129 | case .semicolon: input = .init(";") 130 | case .quote: input = .init("'") 131 | case .openBracket: input = .init("[") 132 | case .closeBracket: input = .init("]") 133 | case .backslash: input = .init("\\") 134 | case .nonUSBackslash: input = .init("|") 135 | case .hyphen, .keypadHyphen: input = .init("-") 136 | case .equalSign, .keypadEqualSign: input = .init("=") 137 | case .graveAccentAndTilde: input = .init("`") 138 | 139 | case .keypadPlus: input = .init("+") 140 | case .keypadAsterisk: input = .init("*") 141 | 142 | case .one, .keypad1: input = .init("1") 143 | case .two, .keypad2: input = .init("2") 144 | case .three, .keypad3: input = .init("3") 145 | case .four, .keypad4: input = .init("4") 146 | case .five, .keypad5: input = .init("5") 147 | case .six, .keypad6: input = .init("6") 148 | case .seven, .keypad7: input = .init("7") 149 | case .eight, .keypad8: input = .init("8") 150 | case .nine, .keypad9: input = .init("9") 151 | case .zero, .keypad0: input = .init("0") 152 | 153 | default: 154 | // Catch-all for single letters. 155 | guard let key = buttonInput.description.components(separatedBy: .whitespacesAndNewlines).first, key.count == 1 else { return } 156 | input = Input(stringValue: key.lowercased()) 157 | } 158 | 159 | if isActive 160 | { 161 | self?.activate(input) 162 | } 163 | else 164 | { 165 | self?.deactivate(input) 166 | } 167 | } 168 | } 169 | } 170 | 171 | public extension KeyboardGameController 172 | { 173 | override func keyPressesBegan(_ presses: Set, with event: UIEvent) 174 | { 175 | // Ignore unless using legacy keyboard handling. 176 | guard self.keyboard == nil else { return } 177 | 178 | for press in presses 179 | { 180 | let input = Input(press.key) 181 | self.activate(input) 182 | } 183 | } 184 | 185 | override func keyPressesEnded(_ presses: Set, with event: UIEvent) 186 | { 187 | // Ignore unless using legacy keyboard handling. 188 | guard self.keyboard == nil else { return } 189 | 190 | for press in presses 191 | { 192 | let input = Input(press.key) 193 | self.deactivate(input) 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Video/VideoManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoManager.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 3/16/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Accelerate 11 | import CoreImage 12 | import GLKit 13 | 14 | protocol VideoProcessor 15 | { 16 | var videoFormat: VideoFormat { get } 17 | var videoBuffer: UnsafeMutablePointer? { get } 18 | 19 | var viewport: CGRect { get set } 20 | 21 | func prepare() 22 | func processFrame() -> CIImage? 23 | } 24 | 25 | extension VideoProcessor 26 | { 27 | var correctedViewport: CGRect? { 28 | guard self.viewport != .zero else { return nil } 29 | 30 | let viewport = CGRect(x: self.viewport.minX, y: self.videoFormat.dimensions.height - self.viewport.height, 31 | width: self.viewport.width, height: self.viewport.height) 32 | return viewport 33 | } 34 | } 35 | 36 | public class VideoManager: NSObject, VideoRendering 37 | { 38 | public internal(set) var videoFormat: VideoFormat { 39 | didSet { 40 | self.updateProcessor() 41 | } 42 | } 43 | 44 | public let options: [EmulatorCore.Option: Any] 45 | 46 | public var viewport: CGRect = .zero { 47 | didSet { 48 | self.processor.viewport = self.viewport 49 | } 50 | } 51 | 52 | public var gameViews: Set { 53 | return _gameViews.setRepresentation as! Set 54 | } 55 | private let _gameViews: NSHashTable = NSHashTable.weakObjects() 56 | 57 | public var isEnabled = true 58 | 59 | private let eaglContext: EAGLContext? 60 | private let ciContext: CIContext 61 | 62 | private var processor: VideoProcessor 63 | @NSCopying private var processedImage: CIImage? 64 | @NSCopying private var displayedImage: CIImage? // Can only accurately snapshot rendered images. 65 | 66 | private lazy var renderThread = RenderThread(action: { [weak self] in 67 | self?._render() 68 | }) 69 | 70 | public init(videoFormat: VideoFormat, options: [EmulatorCore.Option: Any] = [:]) 71 | { 72 | self.videoFormat = videoFormat 73 | self.options = options 74 | 75 | switch videoFormat.format 76 | { 77 | case .bitmap: 78 | self.processor = BitmapProcessor(videoFormat: videoFormat) 79 | 80 | if let prefersMetal = options[.metal] as? Bool, prefersMetal 81 | { 82 | self.ciContext = CIContext(options: [.workingColorSpace: NSNull()]) 83 | self.eaglContext = nil 84 | } 85 | else 86 | { 87 | let context = EAGLContext(api: .openGLES3)! 88 | self.ciContext = CIContext(eaglContext: context, options: [.workingColorSpace: NSNull()]) 89 | self.eaglContext = context 90 | } 91 | 92 | case .openGLES2: 93 | let context = EAGLContext(api: .openGLES2)! 94 | self.ciContext = CIContext(eaglContext: context, options: [.workingColorSpace: NSNull()]) 95 | self.processor = OpenGLESProcessor(videoFormat: videoFormat, context: context) 96 | self.eaglContext = context 97 | 98 | case .openGLES3: 99 | let context = EAGLContext(api: .openGLES3)! 100 | self.ciContext = CIContext(eaglContext: context, options: [.workingColorSpace: NSNull()]) 101 | self.processor = OpenGLESProcessor(videoFormat: videoFormat, context: context) 102 | self.eaglContext = context 103 | } 104 | 105 | super.init() 106 | 107 | self.renderThread.start() 108 | } 109 | 110 | private func updateProcessor() 111 | { 112 | switch self.videoFormat.format 113 | { 114 | case .bitmap: 115 | self.processor = BitmapProcessor(videoFormat: self.videoFormat) 116 | 117 | case .openGLES2, .openGLES3: 118 | guard let processor = self.processor as? OpenGLESProcessor else { return } 119 | processor.videoFormat = self.videoFormat 120 | } 121 | 122 | self.processor.viewport = self.viewport 123 | } 124 | 125 | deinit 126 | { 127 | self.renderThread.cancel() 128 | } 129 | } 130 | 131 | public extension VideoManager 132 | { 133 | func add(_ gameView: GameView) 134 | { 135 | guard !self.gameViews.contains(gameView) else { return } 136 | 137 | gameView.eaglContext = self.eaglContext 138 | self._gameViews.add(gameView) 139 | } 140 | 141 | func remove(_ gameView: GameView) 142 | { 143 | self._gameViews.remove(gameView) 144 | } 145 | } 146 | 147 | public extension VideoManager 148 | { 149 | var videoBuffer: UnsafeMutablePointer? { 150 | return self.processor.videoBuffer 151 | } 152 | 153 | func prepare() 154 | { 155 | self.processor.prepare() 156 | } 157 | 158 | func processFrame() 159 | { 160 | guard self.isEnabled else { return } 161 | 162 | autoreleasepool { 163 | self.processedImage = self.processor.processFrame() 164 | } 165 | } 166 | 167 | func render() 168 | { 169 | guard self.isEnabled else { return } 170 | 171 | guard let image = self.processedImage else { return } 172 | 173 | // Skip frame if previous frame is not finished rendering. 174 | guard self.renderThread.wait(timeout: .now()) == .success else { return } 175 | 176 | self.displayedImage = image 177 | 178 | self.renderThread.run() 179 | } 180 | 181 | func snapshot() -> UIImage? 182 | { 183 | guard let displayedImage = self.displayedImage else { return nil } 184 | 185 | let imageWidth = Int(displayedImage.extent.width) 186 | let imageHeight = Int(displayedImage.extent.height) 187 | let capacity = imageWidth * imageHeight * 4 188 | 189 | let imageBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: capacity, alignment: 1) 190 | defer { imageBuffer.deallocate() } 191 | 192 | guard let baseAddress = imageBuffer.baseAddress, let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) else { return nil } 193 | 194 | // Must render to raw buffer first so we can set CGImageAlphaInfo.noneSkipLast flag when creating CGImage. 195 | // Otherwise, some parts of images may incorrectly be transparent. 196 | self.ciContext.render(displayedImage, toBitmap: baseAddress, rowBytes: imageWidth * 4, bounds: displayedImage.extent, format: .RGBA8, colorSpace: colorSpace) 197 | 198 | let data = Data(bytes: baseAddress, count: imageBuffer.count) 199 | let bitmapInfo: CGBitmapInfo = [CGBitmapInfo.byteOrder32Big, CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue)] 200 | 201 | guard 202 | let dataProvider = CGDataProvider(data: data as CFData), 203 | let cgImage = CGImage(width: imageWidth, height: imageHeight, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: imageWidth * 4, space: colorSpace, bitmapInfo: bitmapInfo, provider: dataProvider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) 204 | else { return nil } 205 | 206 | let image = UIImage(cgImage: cgImage) 207 | return image 208 | } 209 | } 210 | 211 | private extension VideoManager 212 | { 213 | func _render() 214 | { 215 | for gameView in self.gameViews 216 | { 217 | if let exclusiveVideoManager = gameView.exclusiveVideoManager 218 | { 219 | guard exclusiveVideoManager == self else { continue } 220 | } 221 | 222 | gameView.inputImage = self.displayedImage 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /DeltaCore/UI/Controller/ButtonsInputView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonsInputView.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 8/4/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ButtonsInputView: UIView 12 | { 13 | var isHapticFeedbackEnabled = true 14 | 15 | var items: [ControllerSkin.Item]? 16 | 17 | var activateInputsHandler: ((Set) -> Void)? 18 | var deactivateInputsHandler: ((Set) -> Void)? 19 | 20 | var image: UIImage? { 21 | get { 22 | return self.imageView.image 23 | } 24 | set { 25 | self.imageView.image = newValue 26 | } 27 | } 28 | 29 | private let imageView = UIImageView(frame: .zero) 30 | 31 | private let feedbackGenerator = UIImpactFeedbackGenerator(style: .medium) 32 | 33 | private var touchInputsMappingDictionary: [UITouch: Set] = [:] 34 | private var previousTouchInputs = Set() 35 | private var touchInputs: Set { 36 | return self.touchInputsMappingDictionary.values.reduce(Set(), { $0.union($1) }) 37 | } 38 | 39 | override var intrinsicContentSize: CGSize { 40 | return self.imageView.intrinsicContentSize 41 | } 42 | 43 | override init(frame: CGRect) 44 | { 45 | super.init(frame: frame) 46 | 47 | self.isMultipleTouchEnabled = true 48 | 49 | self.feedbackGenerator.prepare() 50 | 51 | self.imageView.translatesAutoresizingMaskIntoConstraints = false 52 | self.addSubview(self.imageView) 53 | 54 | NSLayoutConstraint.activate([self.imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor), 55 | self.imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor), 56 | self.imageView.topAnchor.constraint(equalTo: self.topAnchor), 57 | self.imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor)]) 58 | } 59 | 60 | required init?(coder aDecoder: NSCoder) { 61 | fatalError("init(coder:) has not been implemented") 62 | } 63 | 64 | public override func touchesBegan(_ touches: Set, with event: UIEvent?) 65 | { 66 | for touch in touches 67 | { 68 | self.touchInputsMappingDictionary[touch] = [] 69 | } 70 | 71 | self.updateInputs(for: touches) 72 | } 73 | 74 | public override func touchesMoved(_ touches: Set, with event: UIEvent?) 75 | { 76 | self.updateInputs(for: touches) 77 | } 78 | 79 | public override func touchesEnded(_ touches: Set, with event: UIEvent?) 80 | { 81 | for touch in touches 82 | { 83 | self.touchInputsMappingDictionary[touch] = nil 84 | } 85 | 86 | self.updateInputs(for: touches) 87 | } 88 | 89 | public override func touchesCancelled(_ touches: Set, with event: UIEvent?) 90 | { 91 | return self.touchesEnded(touches, with: event) 92 | } 93 | } 94 | 95 | extension ButtonsInputView 96 | { 97 | func inputs(at point: CGPoint) -> [Input]? 98 | { 99 | guard let items = self.items else { return nil } 100 | 101 | var point = point 102 | point.x /= self.bounds.width 103 | point.y /= self.bounds.height 104 | 105 | var inputs: [Input] = [] 106 | 107 | for item in items 108 | { 109 | guard item.extendedFrame.contains(point) else { continue } 110 | 111 | switch item.inputs 112 | { 113 | // Don't return inputs for thumbsticks or touch screens since they're handled separately. 114 | case .directional where item.kind == .thumbstick: break 115 | case .touch: break 116 | 117 | case .standard(let itemInputs): 118 | inputs.append(contentsOf: itemInputs) 119 | 120 | case let .directional(up, down, left, right): 121 | 122 | let divisor: CGFloat 123 | if case .thumbstick = item.kind 124 | { 125 | divisor = 2.0 126 | } 127 | else 128 | { 129 | divisor = 3.0 130 | } 131 | 132 | let topRect = CGRect(x: item.extendedFrame.minX, y: item.extendedFrame.minY, width: item.extendedFrame.width, height: (item.frame.height / divisor) + (item.frame.minY - item.extendedFrame.minY)) 133 | let bottomRect = CGRect(x: item.extendedFrame.minX, y: item.frame.maxY - item.frame.height / divisor, width: item.extendedFrame.width, height: (item.frame.height / divisor) + (item.extendedFrame.maxY - item.frame.maxY)) 134 | let leftRect = CGRect(x: item.extendedFrame.minX, y: item.extendedFrame.minY, width: (item.frame.width / divisor) + (item.frame.minX - item.extendedFrame.minX), height: item.extendedFrame.height) 135 | let rightRect = CGRect(x: item.frame.maxX - item.frame.width / divisor, y: item.extendedFrame.minY, width: (item.frame.width / divisor) + (item.extendedFrame.maxX - item.frame.maxX), height: item.extendedFrame.height) 136 | 137 | if topRect.contains(point) 138 | { 139 | inputs.append(up) 140 | } 141 | 142 | if bottomRect.contains(point) 143 | { 144 | inputs.append(down) 145 | } 146 | 147 | if leftRect.contains(point) 148 | { 149 | inputs.append(left) 150 | } 151 | 152 | if rightRect.contains(point) 153 | { 154 | inputs.append(right) 155 | } 156 | } 157 | } 158 | 159 | return inputs 160 | } 161 | } 162 | 163 | private extension ButtonsInputView 164 | { 165 | func updateInputs(for touches: Set) 166 | { 167 | // Don't add the touches if it has been removed in touchesEnded:/touchesCancelled: 168 | for touch in touches where self.touchInputsMappingDictionary[touch] != nil 169 | { 170 | guard touch.view == self else { continue } 171 | 172 | let point = touch.location(in: self) 173 | let inputs = Set((self.inputs(at: point) ?? []).map { AnyInput($0) }) 174 | 175 | let menuInput = AnyInput(stringValue: StandardGameControllerInput.menu.stringValue, intValue: nil, type: .controller(.controllerSkin)) 176 | if inputs.contains(menuInput) 177 | { 178 | // If the menu button is located at this position, ignore all other inputs that might be overlapping. 179 | self.touchInputsMappingDictionary[touch] = [menuInput] 180 | } 181 | else 182 | { 183 | self.touchInputsMappingDictionary[touch] = Set(inputs) 184 | } 185 | } 186 | 187 | let activatedInputs = self.touchInputs.subtracting(self.previousTouchInputs) 188 | let deactivatedInputs = self.previousTouchInputs.subtracting(self.touchInputs) 189 | 190 | // We must update previousTouchInputs *before* calling activate() and deactivate(). 191 | // Otherwise, race conditions that cause duplicate touches from activate() or deactivate() calls can result in various bugs. 192 | self.previousTouchInputs = self.touchInputs 193 | 194 | if !activatedInputs.isEmpty 195 | { 196 | self.activateInputsHandler?(activatedInputs) 197 | 198 | if self.isHapticFeedbackEnabled 199 | { 200 | switch UIDevice.current.feedbackSupportLevel 201 | { 202 | case .feedbackGenerator: self.feedbackGenerator.impactOccurred() 203 | case .basic, .unsupported: UIDevice.current.vibrate() 204 | } 205 | } 206 | } 207 | 208 | if !deactivatedInputs.isEmpty 209 | { 210 | self.deactivateInputsHandler?(deactivatedInputs) 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /DeltaCore/UI/Controller/ThumbstickInputView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThumbstickInputView.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 4/18/19. 6 | // Copyright © 2019 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import simd 11 | 12 | extension ThumbstickInputView 13 | { 14 | private enum Direction 15 | { 16 | case up 17 | case down 18 | case left 19 | case right 20 | 21 | init?(xAxis: Double, yAxis: Double, threshold: Double) 22 | { 23 | let deadzone = -threshold...threshold 24 | switch (xAxis, yAxis) 25 | { 26 | case (deadzone, deadzone): return nil 27 | case (...0, deadzone): self = .left 28 | case (0..., deadzone): self = .right 29 | case (deadzone, ...0): self = .down 30 | case (deadzone, 0...): self = .up 31 | default: return nil 32 | } 33 | } 34 | } 35 | } 36 | 37 | class ThumbstickInputView: UIView 38 | { 39 | var isHapticFeedbackEnabled = true 40 | 41 | var valueChangedHandler: ((Double, Double) -> Void)? 42 | 43 | var thumbstickImage: UIImage? { 44 | didSet { 45 | self.update() 46 | } 47 | } 48 | 49 | var thumbstickSize: CGSize? { 50 | didSet { 51 | self.update() 52 | } 53 | } 54 | 55 | private let imageView = UIImageView(image: nil) 56 | private let panGestureRecognizer = ImmediatePanGestureRecognizer(target: nil, action: nil) 57 | 58 | private let lightFeedbackGenerator = UISelectionFeedbackGenerator() 59 | private let mediumFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) 60 | 61 | private var isActivated = false 62 | 63 | private var trackingOrigin: CGPoint? 64 | private var previousDirection: Direction? 65 | 66 | private var isTracking: Bool { 67 | return self.trackingOrigin != nil 68 | } 69 | 70 | override init(frame: CGRect) 71 | { 72 | super.init(frame: frame) 73 | 74 | self.panGestureRecognizer.addTarget(self, action: #selector(ThumbstickInputView.handlePanGesture(_:))) 75 | self.panGestureRecognizer.delaysTouchesBegan = true 76 | self.panGestureRecognizer.cancelsTouchesInView = true 77 | self.addGestureRecognizer(self.panGestureRecognizer) 78 | 79 | self.addSubview(self.imageView) 80 | 81 | self.update() 82 | } 83 | 84 | required init?(coder aDecoder: NSCoder) 85 | { 86 | fatalError("init(coder:) has not been implemented") 87 | } 88 | 89 | override func layoutSubviews() 90 | { 91 | super.layoutSubviews() 92 | 93 | self.update() 94 | } 95 | } 96 | 97 | private extension ThumbstickInputView 98 | { 99 | @objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) 100 | { 101 | switch gestureRecognizer.state 102 | { 103 | case .began: 104 | let location = gestureRecognizer.location(in: self) 105 | self.trackingOrigin = location 106 | 107 | if self.isHapticFeedbackEnabled 108 | { 109 | self.lightFeedbackGenerator.prepare() 110 | self.mediumFeedbackGenerator.prepare() 111 | } 112 | 113 | self.update() 114 | 115 | case .changed: 116 | // When initially tracking the gesture, we calculate the translation 117 | // relative to where the user began the pan gesture. 118 | // This works well, but becomes weird once we leave the bounds then return later, 119 | // since it's more obvious at that point if the thumbstick position doesn't match the user's finger. 120 | // 121 | // To compensate, once we've left the bounds (and have reached maximum translation), 122 | // we reset the origin we're using for calculation to 0. 123 | // This won't change the visual position of the thumbstick since it's snapped to the edge, 124 | // but will correctly track user's finger upon re-entering the bounds. 125 | 126 | guard var origin = self.trackingOrigin else { break } 127 | 128 | let location = gestureRecognizer.location(in: self) 129 | let translationX = location.x - origin.x 130 | let translationY = location.y - origin.y 131 | 132 | let x = origin.x + translationX 133 | let y = origin.y + translationY 134 | 135 | let horizontalRange = self.bounds.minX...self.bounds.maxX 136 | let verticalRange = self.bounds.minY...self.bounds.maxY 137 | 138 | if !horizontalRange.contains(x) && abs(translationX) >= self.bounds.midX 139 | { 140 | origin.x = self.bounds.midX 141 | } 142 | 143 | if !verticalRange.contains(y) && abs(translationY) >= self.bounds.midY 144 | { 145 | origin.y = self.bounds.midY 146 | } 147 | 148 | let translation = CGPoint(x: translationX, y: translationY) 149 | self.update(translation) 150 | 151 | self.trackingOrigin = origin 152 | 153 | case .ended, .cancelled: 154 | 155 | if self.isHapticFeedbackEnabled 156 | { 157 | self.mediumFeedbackGenerator.impactOccurred() 158 | } 159 | 160 | self.update() 161 | 162 | self.trackingOrigin = nil 163 | self.isActivated = false 164 | self.previousDirection = nil 165 | 166 | default: break 167 | } 168 | } 169 | 170 | func update(_ translation: CGPoint = CGPoint(x: 0, y: 0)) 171 | { 172 | let center = SIMD2(Double(self.bounds.midX), Double(self.bounds.midY)) 173 | let point = SIMD2(Double(translation.x), Double(translation.y)) 174 | 175 | self.imageView.image = self.thumbstickImage 176 | 177 | if let size = self.thumbstickSize 178 | { 179 | self.imageView.bounds.size = CGSize(width: size.width, height: size.height) 180 | } 181 | else 182 | { 183 | self.imageView.sizeToFit() 184 | } 185 | 186 | guard !self.bounds.isEmpty, self.isTracking else { 187 | self.imageView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY) 188 | return 189 | } 190 | 191 | let maximumDistance = Double(self.bounds.midX) 192 | let distance = min(simd_length(point), maximumDistance) 193 | 194 | let angle = atan2(point.y, point.x) 195 | 196 | var adjustedX = distance * cos(angle) 197 | adjustedX += center.x 198 | 199 | var adjustedY = distance * sin(angle) 200 | adjustedY += center.y 201 | 202 | let insetSideLength = maximumDistance / sqrt(2) 203 | let insetFrame = CGRect(x: center.x - insetSideLength / 2, 204 | y: center.y - insetSideLength / 2, 205 | width: insetSideLength, 206 | height: insetSideLength) 207 | 208 | let threshold = 0.1 209 | 210 | var xAxis = Double((CGFloat(adjustedX) - insetFrame.minX) / insetFrame.width) 211 | xAxis = max(xAxis, 0) 212 | xAxis = min(xAxis, 1) 213 | xAxis = (xAxis * 2) - 1 // Convert range from [0, 1] to [-1, 1]. 214 | 215 | if abs(xAxis) < threshold 216 | { 217 | xAxis = 0 218 | } 219 | 220 | var yAxis = Double((CGFloat(adjustedY) - insetFrame.minY) / insetFrame.height) 221 | yAxis = max(yAxis, 0) 222 | yAxis = min(yAxis, 1) 223 | yAxis = -((yAxis * 2) - 1) // Convert range from [0, 1] to [-1, 1], then invert it (due to flipped coordinates). 224 | 225 | if abs(yAxis) < threshold 226 | { 227 | yAxis = 0 228 | } 229 | 230 | let magnitude = simd_length(SIMD2(xAxis, yAxis)) 231 | let isActivated = (magnitude > 0.1) 232 | 233 | if let direction = Direction(xAxis: xAxis, yAxis: yAxis, threshold: threshold) 234 | { 235 | if self.previousDirection != direction && self.isHapticFeedbackEnabled 236 | { 237 | self.mediumFeedbackGenerator.impactOccurred() 238 | } 239 | 240 | self.previousDirection = direction 241 | } 242 | else 243 | { 244 | if isActivated && !self.isActivated && self.isHapticFeedbackEnabled 245 | { 246 | self.lightFeedbackGenerator.selectionChanged() 247 | } 248 | 249 | self.previousDirection = nil 250 | } 251 | 252 | self.isActivated = isActivated 253 | 254 | self.imageView.center = CGPoint(x: adjustedX, y: adjustedY) 255 | self.valueChangedHandler?(xAxis, yAxis) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /DeltaCore/Game Controllers/ExternalGameControllerManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExternalGameControllerManager.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 8/20/15. 6 | // Copyright © 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import GameController 11 | 12 | private let ExternalKeyboardStatusDidChange: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = { 13 | (notificationCenter, observer, name, object, userInfo) in 14 | 15 | if ExternalGameControllerManager.shared.isKeyboardConnected 16 | { 17 | NotificationCenter.default.post(name: .externalKeyboardDidConnect, object: nil) 18 | } 19 | else 20 | { 21 | NotificationCenter.default.post(name: .externalKeyboardDidDisconnect, object: nil) 22 | } 23 | } 24 | 25 | public extension Notification.Name 26 | { 27 | static let externalGameControllerDidConnect = Notification.Name("ExternalGameControllerDidConnectNotification") 28 | static let externalGameControllerDidDisconnect = Notification.Name("ExternalGameControllerDidDisconnectNotification") 29 | 30 | static let externalKeyboardDidConnect = Notification.Name("ExternalKeyboardDidConnect") 31 | static let externalKeyboardDidDisconnect = Notification.Name("ExternalKeyboardDidDisconnect") 32 | } 33 | 34 | public class ExternalGameControllerManager: UIResponder 35 | { 36 | public static let shared = ExternalGameControllerManager() 37 | 38 | //MARK: - Properties - 39 | /** Properties **/ 40 | public private(set) var connectedControllers: [GameController] = [] 41 | 42 | public var automaticallyAssignsPlayerIndexes: Bool 43 | 44 | internal var keyboardController: KeyboardGameController? { 45 | let keyboardController = self.connectedControllers.lazy.compactMap { $0 as? KeyboardGameController }.first 46 | return keyboardController 47 | } 48 | 49 | internal var prefersModernKeyboardHandling: Bool { 50 | if ProcessInfo.processInfo.isiOSAppOnMac 51 | { 52 | // Legacy keyboard handling doesn't work on macOS, so use modern handling instead. 53 | // It's still in development, but better than nothing. 54 | return true 55 | } 56 | else if #available(iOS 26.0, *) 57 | { 58 | // Legacy keyboard handling no longer works on iOS 26, RIP. 59 | return true 60 | } 61 | else 62 | { 63 | return false 64 | } 65 | } 66 | 67 | private var nextAvailablePlayerIndex: Int { 68 | var nextPlayerIndex = -1 69 | 70 | let sortedGameControllers = self.connectedControllers.sorted { ($0.playerIndex ?? -1) < ($1.playerIndex ?? -1) } 71 | for controller in sortedGameControllers 72 | { 73 | let playerIndex = controller.playerIndex ?? -1 74 | 75 | if abs(playerIndex - nextPlayerIndex) > 1 76 | { 77 | break 78 | } 79 | else 80 | { 81 | nextPlayerIndex = playerIndex 82 | } 83 | } 84 | 85 | nextPlayerIndex += 1 86 | 87 | return nextPlayerIndex 88 | } 89 | 90 | private override init() 91 | { 92 | #if targetEnvironment(simulator) 93 | self.automaticallyAssignsPlayerIndexes = false 94 | #else 95 | self.automaticallyAssignsPlayerIndexes = true 96 | #endif 97 | 98 | super.init() 99 | } 100 | } 101 | 102 | //MARK: - Discovery - 103 | /** Discovery **/ 104 | public extension ExternalGameControllerManager 105 | { 106 | func startMonitoring() 107 | { 108 | for controller in GCController.controllers() 109 | { 110 | let externalController = MFiGameController(controller: controller) 111 | self.add(externalController) 112 | } 113 | 114 | if self.isKeyboardConnected 115 | { 116 | let keyboard = self.prefersModernKeyboardHandling ? GCKeyboard.coalesced : nil 117 | let keyboardController = KeyboardGameController(keyboard: keyboard) 118 | self.add(keyboardController) 119 | } 120 | 121 | NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.mfiGameControllerDidConnect(_:)), name: .GCControllerDidConnect, object: nil) 122 | NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.mfiGameControllerDidDisconnect(_:)), name: .GCControllerDidDisconnect, object: nil) 123 | 124 | NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.keyboardDidConnect(_:)), name: .externalKeyboardDidConnect, object: nil) 125 | NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.keyboardDidDisconnect(_:)), name: .externalKeyboardDidDisconnect, object: nil) 126 | 127 | if #available(iOS 14, *) 128 | { 129 | NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.gcKeyboardDidConnect(_:)), name: .GCKeyboardDidConnect, object: nil) 130 | NotificationCenter.default.addObserver(self, selector: #selector(ExternalGameControllerManager.gcKeyboardDidDisconnect(_:)), name: .GCKeyboardDidDisconnect, object: nil) 131 | } 132 | else 133 | { 134 | let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() 135 | CFNotificationCenterAddObserver(notificationCenter, nil, ExternalKeyboardStatusDidChange, "GSEventHardwareKeyboardAttached" as CFString, nil, .deliverImmediately) 136 | } 137 | } 138 | 139 | func stopMonitoring() 140 | { 141 | NotificationCenter.default.removeObserver(self, name: .GCControllerDidConnect, object: nil) 142 | NotificationCenter.default.removeObserver(self, name: .GCControllerDidDisconnect, object: nil) 143 | 144 | NotificationCenter.default.removeObserver(self, name: .externalKeyboardDidConnect, object: nil) 145 | NotificationCenter.default.removeObserver(self, name: .externalKeyboardDidDisconnect, object: nil) 146 | 147 | self.connectedControllers.removeAll() 148 | } 149 | 150 | func startWirelessControllerDiscovery(withCompletionHandler completionHandler: (() -> Void)?) 151 | { 152 | GCController.startWirelessControllerDiscovery(completionHandler: completionHandler) 153 | } 154 | 155 | func stopWirelessControllerDiscovery() 156 | { 157 | GCController.stopWirelessControllerDiscovery() 158 | } 159 | } 160 | 161 | //MARK: - External Keyboard - 162 | public extension ExternalGameControllerManager 163 | { 164 | // Implementation based on Ian McDowell's tweet: https://twitter.com/ian_mcdowell/status/844572113759547392 165 | var isKeyboardConnected: Bool { 166 | if #available(iOS 14, *) 167 | { 168 | return GCKeyboard.coalesced != nil 169 | } 170 | else 171 | { 172 | guard let uiKeyboardClass: AnyObject = NSClassFromString("UIKeyboard") else { return false } 173 | 174 | let selector = NSSelectorFromString("shouldMinimizeForHardwareKeyboard") 175 | guard uiKeyboardClass.responds(to: selector) else { return false } 176 | 177 | if let _ = uiKeyboardClass.perform(selector) 178 | { 179 | // Returns non-nil value when true, so return true ourselves. 180 | return true 181 | } 182 | 183 | return false 184 | } 185 | } 186 | 187 | override func keyPressesBegan(_ presses: Set, with event: UIEvent) 188 | { 189 | for case let keyboardController as KeyboardGameController in self.connectedControllers 190 | { 191 | keyboardController.keyPressesBegan(presses, with: event) 192 | } 193 | } 194 | 195 | override func keyPressesEnded(_ presses: Set, with event: UIEvent) 196 | { 197 | for case let keyboardController as KeyboardGameController in self.connectedControllers 198 | { 199 | keyboardController.keyPressesEnded(presses, with: event) 200 | } 201 | } 202 | } 203 | 204 | //MARK: - Managing Controllers - 205 | private extension ExternalGameControllerManager 206 | { 207 | func add(_ controller: GameController) 208 | { 209 | if self.automaticallyAssignsPlayerIndexes 210 | { 211 | let playerIndex = self.nextAvailablePlayerIndex 212 | controller.playerIndex = playerIndex 213 | } 214 | 215 | self.connectedControllers.append(controller) 216 | 217 | NotificationCenter.default.post(name: .externalGameControllerDidConnect, object: controller) 218 | } 219 | 220 | func remove(_ controller: GameController) 221 | { 222 | guard let index = self.connectedControllers.firstIndex(where: { $0.isEqual(controller) }) else { return } 223 | 224 | self.connectedControllers.remove(at: index) 225 | 226 | NotificationCenter.default.post(name: .externalGameControllerDidDisconnect, object: controller) 227 | } 228 | } 229 | 230 | //MARK: - MFi Game Controllers - 231 | private extension ExternalGameControllerManager 232 | { 233 | @objc func mfiGameControllerDidConnect(_ notification: Notification) 234 | { 235 | guard let controller = notification.object as? GCController else { return } 236 | 237 | let externalController = MFiGameController(controller: controller) 238 | self.add(externalController) 239 | } 240 | 241 | @objc func mfiGameControllerDidDisconnect(_ notification: Notification) 242 | { 243 | guard let controller = notification.object as? GCController else { return } 244 | 245 | for externalController in self.connectedControllers 246 | { 247 | guard let mfiController = externalController as? MFiGameController else { continue } 248 | 249 | if mfiController.controller == controller 250 | { 251 | self.remove(externalController) 252 | } 253 | } 254 | } 255 | 256 | @available(iOS 14.0, *) 257 | @objc func gcKeyboardDidConnect(_ notification: Notification) 258 | { 259 | NotificationCenter.default.post(name: .externalKeyboardDidConnect, object: nil) 260 | } 261 | 262 | @available(iOS 14.0, *) 263 | @objc func gcKeyboardDidDisconnect(_ notification: Notification) 264 | { 265 | NotificationCenter.default.post(name: .externalKeyboardDidDisconnect, object: nil) 266 | } 267 | } 268 | 269 | //MARK: - Keyboard Game Controllers - 270 | private extension ExternalGameControllerManager 271 | { 272 | @objc func keyboardDidConnect(_ notification: Notification) 273 | { 274 | guard self.keyboardController == nil else { return } 275 | 276 | let keyboard = self.prefersModernKeyboardHandling ? GCKeyboard.coalesced : nil 277 | let keyboardController = KeyboardGameController(keyboard: keyboard) 278 | self.add(keyboardController) 279 | } 280 | 281 | @objc func keyboardDidDisconnect(_ notification: Notification) 282 | { 283 | guard let keyboardController = self.keyboardController else { return } 284 | 285 | self.remove(keyboardController) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /DeltaCore/Game Controllers/MFi/MFiGameController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MFiGameController.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 7/22/15. 6 | // Copyright © 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | import GameController 10 | 11 | public extension GameControllerInputType 12 | { 13 | static let mfi = GameControllerInputType("mfi") 14 | } 15 | 16 | extension MFiGameController 17 | { 18 | private enum ProductCategory: String 19 | { 20 | case mfi = "MFi" 21 | 22 | case joyConL = "Nintendo Switch Joy-Con (L)" 23 | case joyConR = "Nintendo Switch Joy-Con (R)" 24 | case joyConsCombined = "Nintendo Switch Joy-Con (L/R)" 25 | 26 | case switchPro = "Switch Pro Controller" 27 | case switchOnlineNES = "Switch NES Controller" 28 | case switchOnlineSNES = "Switch SNES Controller" 29 | } 30 | } 31 | 32 | extension MFiGameController 33 | { 34 | public enum Input: String, Codable 35 | { 36 | case menu 37 | 38 | case up 39 | case down 40 | case left 41 | case right 42 | 43 | case leftThumbstickUp 44 | case leftThumbstickDown 45 | case leftThumbstickLeft 46 | case leftThumbstickRight 47 | 48 | case rightThumbstickUp 49 | case rightThumbstickDown 50 | case rightThumbstickLeft 51 | case rightThumbstickRight 52 | 53 | case a 54 | case b 55 | case x 56 | case y 57 | 58 | case leftShoulder 59 | case leftTrigger 60 | 61 | case rightShoulder 62 | case rightTrigger 63 | 64 | case start 65 | case select 66 | } 67 | } 68 | 69 | extension MFiGameController.Input: Input 70 | { 71 | public var type: InputType { 72 | return .controller(.mfi) 73 | } 74 | 75 | public var isContinuous: Bool { 76 | switch self 77 | { 78 | case .leftThumbstickUp, .leftThumbstickDown, .leftThumbstickLeft, .leftThumbstickRight: return true 79 | case .rightThumbstickUp, .rightThumbstickDown, .rightThumbstickLeft, .rightThumbstickRight: return true 80 | default: return false 81 | } 82 | } 83 | } 84 | 85 | public class MFiGameController: NSObject, GameController 86 | { 87 | //MARK: - Properties - 88 | /** Properties **/ 89 | public let controller: GCController 90 | 91 | public var name: String { 92 | return self.controller.vendorName ?? NSLocalizedString("MFi Controller", comment: "") 93 | } 94 | 95 | public var playerIndex: Int? { 96 | get { 97 | switch self.controller.playerIndex 98 | { 99 | case .indexUnset: return nil 100 | case .index1: return 0 101 | case .index2: return 1 102 | case .index3: return 2 103 | case .index4: return 3 104 | @unknown default: return nil 105 | } 106 | } 107 | set { 108 | switch newValue 109 | { 110 | case .some(0): self.controller.playerIndex = .index1 111 | case .some(1): self.controller.playerIndex = .index2 112 | case .some(2): self.controller.playerIndex = .index3 113 | case .some(3): self.controller.playerIndex = .index4 114 | default: self.controller.playerIndex = .indexUnset 115 | } 116 | } 117 | } 118 | 119 | public let inputType: GameControllerInputType = .mfi 120 | 121 | public private(set) lazy var defaultInputMapping: GameControllerInputMappingProtocol? = { 122 | guard let fileURL = Bundle.resources.url(forResource: "MFiGameController", withExtension: "deltamapping") else { 123 | fatalError("MFiGameController.deltamapping does not exist.") 124 | } 125 | 126 | do 127 | { 128 | let inputMapping = try GameControllerInputMapping(fileURL: fileURL) 129 | return inputMapping 130 | } 131 | catch 132 | { 133 | print(error) 134 | fatalError("MFiGameController.deltamapping does not exist.") 135 | } 136 | }() 137 | 138 | //MARK: - Initializers - 139 | /** Initializers **/ 140 | public init(controller: GCController) 141 | { 142 | self.controller = controller 143 | 144 | super.init() 145 | 146 | let inputChangedHandler: (_ input: MFiGameController.Input, _ pressed: Bool) -> Void = { [unowned self] (input, pressed) in 147 | if pressed 148 | { 149 | self.activate(input) 150 | } 151 | else 152 | { 153 | self.deactivate(input) 154 | } 155 | } 156 | 157 | let thumbstickChangedHandler: (_ input1: MFiGameController.Input, _ input2: MFiGameController.Input, _ value: Float) -> Void = { [unowned self] (input1, input2, value) in 158 | 159 | switch value 160 | { 161 | case ..<0: 162 | self.activate(input1, value: Double(-value)) 163 | self.deactivate(input2) 164 | 165 | case 0: 166 | self.deactivate(input1) 167 | self.deactivate(input2) 168 | 169 | default: 170 | self.deactivate(input1) 171 | self.activate(input2, value: Double(value)) 172 | } 173 | } 174 | 175 | let profile = self.controller.physicalInputProfile 176 | profile.buttons[GCInputButtonA]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.a, pressed) } 177 | profile.buttons[GCInputButtonB]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.b, pressed) } 178 | profile.buttons[GCInputButtonX]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.x, pressed) } 179 | profile.buttons[GCInputButtonY]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.y, pressed) } 180 | 181 | profile.buttons[GCInputLeftShoulder]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.leftShoulder, pressed) } 182 | profile.buttons[GCInputLeftTrigger]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.leftTrigger, pressed) } 183 | profile.buttons[GCInputRightShoulder]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.rightShoulder, pressed) } 184 | profile.buttons[GCInputRightTrigger]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.rightTrigger, pressed) } 185 | 186 | // Menu = Primary menu button (Start/+/Menu) 187 | let menuButton = profile.buttons[GCInputButtonMenu] 188 | menuButton?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.menu, pressed) } 189 | 190 | // Options = Secondary menu button (Select/-) 191 | if let optionsButton = profile.buttons[GCInputButtonOptions] 192 | { 193 | optionsButton.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.select, pressed) } 194 | 195 | // .alwaysReceive == asking permission to record screen every time button is pressed as of iOS 16.3 (annoying). 196 | // optionsButton.preferredSystemGestureState = .alwaysReceive 197 | } 198 | 199 | if let dPad = profile.dpads[GCInputDirectionPad] 200 | { 201 | dPad.up.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.up, pressed) } 202 | dPad.down.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.down, pressed) } 203 | dPad.left.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.left, pressed) } 204 | dPad.right.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.right, pressed) } 205 | } 206 | 207 | if let leftThumbstick = profile.dpads[GCInputLeftThumbstick] 208 | { 209 | leftThumbstick.xAxis.valueChangedHandler = { (axis, value) in 210 | thumbstickChangedHandler(.leftThumbstickLeft, .leftThumbstickRight, value) 211 | } 212 | leftThumbstick.yAxis.valueChangedHandler = { (axis, value) in 213 | thumbstickChangedHandler(.leftThumbstickDown, .leftThumbstickUp, value) 214 | } 215 | } 216 | 217 | if let rightThumbstick = profile.dpads[GCInputRightThumbstick] 218 | { 219 | rightThumbstick.xAxis.valueChangedHandler = { (axis, value) in 220 | thumbstickChangedHandler(.rightThumbstickLeft, .rightThumbstickRight, value) 221 | } 222 | rightThumbstick.yAxis.valueChangedHandler = { (axis, value) in 223 | thumbstickChangedHandler(.rightThumbstickDown, .rightThumbstickUp, value) 224 | } 225 | } 226 | 227 | let productCategory = ProductCategory(rawValue: self.controller.productCategory) 228 | switch productCategory 229 | { 230 | case .mfi: 231 | // MFi controllers typically only have one Menu button, so no need to re-map it. 232 | // menuButton?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.menu, pressed) } 233 | break 234 | 235 | case .joyConL, .joyConR: 236 | // Rotate single Joy-Con inputs 90º 237 | profile.buttons[GCInputButtonA]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.b, pressed) } 238 | profile.buttons[GCInputButtonB]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.y, pressed) } 239 | profile.buttons[GCInputButtonX]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.a, pressed) } 240 | profile.buttons[GCInputButtonY]?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.x, pressed) } 241 | 242 | // For some reason, iOS treats the analog stick as a digital dPad input (as of iOS 16.3). 243 | // Re-map to .leftThumbstick instead to work as expected with N64 games. 244 | guard let dPad = profile.dpads[GCInputDirectionPad] else { break } 245 | dPad.xAxis.valueChangedHandler = { (axis, value) in 246 | thumbstickChangedHandler(.leftThumbstickLeft, .leftThumbstickRight, value) 247 | } 248 | dPad.yAxis.valueChangedHandler = { (axis, value) in 249 | thumbstickChangedHandler(.leftThumbstickDown, .leftThumbstickUp, value) 250 | } 251 | 252 | // Remove existing dPad change handlers to avoid duplicate inputs. 253 | dPad.up.pressedChangedHandler = nil 254 | dPad.down.pressedChangedHandler = nil 255 | dPad.left.pressedChangedHandler = nil 256 | dPad.right.pressedChangedHandler = nil 257 | 258 | case .switchOnlineNES, .switchOnlineSNES: 259 | guard var defaultMapping = self.defaultInputMapping as? GameControllerInputMapping else { break } 260 | 261 | // Re-map ZL and ZR buttons to Menu so we can treat Start as regular input. 262 | if productCategory == .switchOnlineNES 263 | { 264 | defaultMapping.set(StandardGameControllerInput.menu, forControllerInput: Input.leftShoulder) 265 | defaultMapping.set(StandardGameControllerInput.menu, forControllerInput: Input.rightShoulder) 266 | } 267 | else 268 | { 269 | defaultMapping.set(StandardGameControllerInput.menu, forControllerInput: Input.leftTrigger) 270 | defaultMapping.set(StandardGameControllerInput.menu, forControllerInput: Input.rightTrigger) 271 | } 272 | 273 | self.defaultInputMapping = defaultMapping 274 | 275 | // Re-map Start button to...Start 276 | menuButton?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.start, pressed) } 277 | 278 | default: 279 | // Home = Home/"Logo" button 280 | guard let homeButton = profile.buttons[GCInputButtonHome] else { break } 281 | 282 | // If controller has Home button, and isn't MFi controller, treat it as Menu button instead. 283 | // e.g. Switch Pro, PlayStation, and Xbox controllers 284 | homeButton.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.menu, pressed) } 285 | 286 | // Disable "Show Game Center" gesture 287 | homeButton.preferredSystemGestureState = .disabled 288 | 289 | // Re-map Menu button to Start 290 | menuButton?.pressedChangedHandler = { (button, value, pressed) in inputChangedHandler(.start, pressed) } 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /DeltaCore/UI/Game/GameView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameView.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 3/16/15. 6 | // Copyright (c) 2015 Riley Testut. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreImage 11 | import GLKit 12 | import MetalKit 13 | import AVFoundation 14 | 15 | // Create wrapper class to prevent exposing GLKView (and its annoying deprecation warnings) to clients. 16 | private class GameViewGLKViewDelegate: NSObject, GLKViewDelegate 17 | { 18 | weak var gameView: GameView? 19 | 20 | init(gameView: GameView) 21 | { 22 | self.gameView = gameView 23 | } 24 | 25 | func glkView(_ view: GLKView, drawIn rect: CGRect) 26 | { 27 | self.gameView?.glkView(view, drawIn: rect) 28 | } 29 | } 30 | 31 | public enum SamplerMode 32 | { 33 | case linear 34 | case nearestNeighbor 35 | } 36 | 37 | public class GameView: UIView 38 | { 39 | public var isEnabled: Bool = true 40 | 41 | // Set to limit rendering to just a specific VideoManager. 42 | public weak var exclusiveVideoManager: VideoManager? 43 | 44 | @NSCopying public var inputImage: CIImage? { 45 | didSet { 46 | if self.inputImage?.extent != oldValue?.extent 47 | { 48 | DispatchQueue.main.async { 49 | self.setNeedsLayout() 50 | } 51 | } 52 | 53 | self.update() 54 | } 55 | } 56 | 57 | @NSCopying public var filter: CIFilter? { 58 | didSet { 59 | guard self.filter != oldValue else { return } 60 | self.update() 61 | } 62 | } 63 | 64 | public var samplerMode: SamplerMode = .nearestNeighbor { 65 | didSet { 66 | self.update() 67 | } 68 | } 69 | 70 | public var outputImage: CIImage? { 71 | guard let inputImage = self.inputImage else { return nil } 72 | 73 | var image: CIImage? 74 | 75 | switch self.samplerMode 76 | { 77 | case .linear: image = inputImage.samplingLinear() 78 | case .nearestNeighbor: image = inputImage.samplingNearest() 79 | } 80 | 81 | if let filter = self.filter 82 | { 83 | filter.setValue(image, forKey: kCIInputImageKey) 84 | image = filter.outputImage 85 | } 86 | 87 | return image 88 | } 89 | 90 | internal var eaglContext: EAGLContext? { 91 | didSet { 92 | os_unfair_lock_lock(&self.lock) 93 | defer { os_unfair_lock_unlock(&self.lock) } 94 | 95 | self.didLayoutSubviews = false 96 | 97 | // For some reason, if we don't explicitly set current EAGLContext to nil, assigning 98 | // to self.glkView may crash if we've already rendered to a game view. 99 | EAGLContext.setCurrent(nil) 100 | 101 | if let eaglContext 102 | { 103 | self.glkView.context = EAGLContext(api: eaglContext.api, sharegroup: eaglContext.sharegroup)! 104 | self.openGLESContext = self.makeOpenGLESContext() 105 | } 106 | 107 | DispatchQueue.main.async { 108 | // layoutSubviews() must be called after setting self.eaglContext before we can display anything. 109 | self.setNeedsLayout() 110 | } 111 | } 112 | } 113 | private lazy var openGLESContext: CIContext = self.makeOpenGLESContext() 114 | private lazy var metalContext: CIContext = self.makeMetalContext() 115 | 116 | private let glkView: GLKView 117 | private lazy var glkViewDelegate = GameViewGLKViewDelegate(gameView: self) 118 | 119 | private let mtkView: MTKView 120 | private let metalDevice = MTLCreateSystemDefaultDevice() 121 | private lazy var metalCommandQueue = self.metalDevice?.makeCommandQueue() 122 | private weak var metalLayer: CAMetalLayer? 123 | 124 | private var lock = os_unfair_lock() 125 | private var didLayoutSubviews = false 126 | private var didRenderInitialFrame = false 127 | private var isRenderingInitialFrame = false 128 | 129 | private var isUsingMetal: Bool { 130 | let isUsingMetal = (self.eaglContext == nil) 131 | return isUsingMetal 132 | } 133 | 134 | public override init(frame: CGRect) 135 | { 136 | let eaglContext = EAGLContext(api: .openGLES2)! 137 | self.glkView = GLKView(frame: CGRect.zero, context: eaglContext) 138 | self.mtkView = MTKView(frame: .zero, device: self.metalDevice) 139 | 140 | super.init(frame: frame) 141 | 142 | self.initialize() 143 | } 144 | 145 | public required init?(coder aDecoder: NSCoder) 146 | { 147 | let eaglContext = EAGLContext(api: .openGLES2)! 148 | self.glkView = GLKView(frame: CGRect.zero, context: eaglContext) 149 | self.mtkView = MTKView(frame: .zero, device: self.metalDevice) 150 | 151 | super.init(coder: aDecoder) 152 | 153 | self.initialize() 154 | } 155 | 156 | private func initialize() 157 | { 158 | self.glkView.frame = self.bounds 159 | self.glkView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 160 | self.glkView.delegate = self.glkViewDelegate 161 | self.glkView.enableSetNeedsDisplay = false 162 | self.addSubview(self.glkView) 163 | 164 | self.mtkView.frame = self.bounds 165 | self.mtkView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 166 | self.mtkView.delegate = self 167 | self.mtkView.enableSetNeedsDisplay = false 168 | self.mtkView.framebufferOnly = false // Must be false to avoid "frameBufferOnly texture not supported for compute" assertion 169 | self.mtkView.isPaused = true 170 | self.addSubview(self.mtkView) 171 | 172 | if let metalLayer = self.mtkView.layer as? CAMetalLayer 173 | { 174 | self.metalLayer = metalLayer 175 | } 176 | } 177 | 178 | public override func didMoveToWindow() 179 | { 180 | if let window = self.window 181 | { 182 | self.glkView.contentScaleFactor = window.screen.scale 183 | self.update() 184 | } 185 | } 186 | 187 | public override func layoutSubviews() 188 | { 189 | super.layoutSubviews() 190 | 191 | if self.outputImage != nil 192 | { 193 | if self.isUsingMetal 194 | { 195 | self.mtkView.isHidden = false 196 | self.glkView.isHidden = true 197 | } 198 | else 199 | { 200 | self.mtkView.isHidden = true 201 | self.glkView.isHidden = false 202 | } 203 | } 204 | else 205 | { 206 | self.mtkView.isHidden = true 207 | self.glkView.isHidden = true 208 | } 209 | 210 | self.didLayoutSubviews = true 211 | } 212 | } 213 | 214 | public extension GameView 215 | { 216 | func snapshot() -> UIImage? 217 | { 218 | // Unfortunately, rendering CIImages doesn't always work when backed by an OpenGLES texture. 219 | // As a workaround, we simply render the view itself into a graphics context the same size 220 | // as our output image. 221 | // 222 | // let cgImage = self.context.createCGImage(outputImage, from: outputImage.extent) 223 | 224 | guard let outputImage = self.outputImage else { return nil } 225 | 226 | let rect = CGRect(origin: .zero, size: outputImage.extent.size) 227 | 228 | let format = UIGraphicsImageRendererFormat() 229 | format.scale = 1.0 230 | format.opaque = true 231 | 232 | let renderer = UIGraphicsImageRenderer(size: rect.size, format: format) 233 | 234 | let snapshot = renderer.image { (context) in 235 | if self.isUsingMetal 236 | { 237 | self.mtkView.drawHierarchy(in: rect, afterScreenUpdates: false) 238 | } 239 | else 240 | { 241 | self.glkView.drawHierarchy(in: rect, afterScreenUpdates: false) 242 | } 243 | } 244 | 245 | return snapshot 246 | } 247 | 248 | func update(for screen: ControllerSkin.Screen) 249 | { 250 | var filters = [CIFilter]() 251 | 252 | if let inputFrame = screen.inputFrame 253 | { 254 | let cropFilter = CIFilter(name: "CICrop", parameters: ["inputRectangle": CIVector(cgRect: inputFrame)])! 255 | filters.append(cropFilter) 256 | } 257 | 258 | if let screenFilters = screen.filters 259 | { 260 | filters.append(contentsOf: screenFilters) 261 | } 262 | 263 | // Always use FilterChain since it has additional logic for chained filters. 264 | let filterChain = filters.isEmpty ? nil : FilterChain(filters: filters) 265 | self.filter = filterChain 266 | } 267 | } 268 | 269 | private extension GameView 270 | { 271 | func makeOpenGLESContext() -> CIContext 272 | { 273 | let context = CIContext(eaglContext: self.glkView.context, options: [.workingColorSpace: NSNull()]) 274 | return context 275 | } 276 | 277 | func makeMetalContext() -> CIContext 278 | { 279 | guard let metalCommandQueue else { 280 | // This should never be called, but just in case we return dummy CIContext. 281 | return CIContext(options: [.workingColorSpace: NSNull()]) 282 | } 283 | 284 | let options: [CIContextOption: Any] = [.workingColorSpace: NSNull(), 285 | .cacheIntermediates: true, 286 | .name: "GameView Context"] 287 | 288 | let context = CIContext(mtlCommandQueue: metalCommandQueue, options: options) 289 | return context 290 | } 291 | 292 | func update() 293 | { 294 | // Calling display when outputImage is nil may crash for OpenGLES-based rendering. 295 | guard self.isEnabled && self.outputImage != nil else { return } 296 | 297 | os_unfair_lock_lock(&self.lock) 298 | defer { os_unfair_lock_unlock(&self.lock) } 299 | 300 | // layoutSubviews() must be called after setting self.eaglContext before we can display anything. 301 | // Otherwise, the app may crash due to race conditions when creating framebuffer from background thread. 302 | guard self.didLayoutSubviews else { return } 303 | 304 | if !self.didRenderInitialFrame 305 | { 306 | if Thread.isMainThread 307 | { 308 | self.render() 309 | self.didRenderInitialFrame = true 310 | } 311 | else if !self.isRenderingInitialFrame 312 | { 313 | // Make sure we don't make multiple calls to glkView.display() before first call returns. 314 | self.isRenderingInitialFrame = true 315 | 316 | DispatchQueue.main.async { 317 | self.render() 318 | self.didRenderInitialFrame = true 319 | self.isRenderingInitialFrame = false 320 | } 321 | } 322 | } 323 | else 324 | { 325 | self.render() 326 | } 327 | } 328 | 329 | func render() 330 | { 331 | if self.isUsingMetal 332 | { 333 | self.mtkView.draw() 334 | } 335 | else 336 | { 337 | self.glkView.display() 338 | } 339 | } 340 | } 341 | 342 | private extension GameView 343 | { 344 | func glkView(_ view: GLKView, drawIn rect: CGRect) 345 | { 346 | glClearColor(0.0, 0.0, 0.0, 1.0) 347 | glClear(UInt32(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)) 348 | 349 | if let outputImage = self.outputImage 350 | { 351 | let bounds = CGRect(x: 0, y: 0, width: self.glkView.drawableWidth, height: self.glkView.drawableHeight) 352 | self.openGLESContext.draw(outputImage, in: bounds, from: outputImage.extent) 353 | } 354 | } 355 | } 356 | 357 | extension GameView: MTKViewDelegate 358 | { 359 | public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) 360 | { 361 | } 362 | 363 | public func draw(in view: MTKView) 364 | { 365 | autoreleasepool { 366 | guard let image = self.outputImage, 367 | let commandBuffer = self.metalCommandQueue?.makeCommandBuffer(), 368 | let currentDrawable = self.metalLayer?.nextDrawable() 369 | else { return } 370 | 371 | let scaleX = view.drawableSize.width / image.extent.width 372 | let scaleY = view.drawableSize.height / image.extent.height 373 | let outputImage = image.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY)) 374 | 375 | do 376 | { 377 | let destination = CIRenderDestination(width: Int(view.drawableSize.width), 378 | height: Int(view.drawableSize.height), 379 | pixelFormat: view.colorPixelFormat, 380 | commandBuffer: nil) { [unowned currentDrawable] () -> MTLTexture in 381 | // Lazily return texture to prevent hangs due to waiting for previous command to finish. 382 | let texture = currentDrawable.texture 383 | return texture 384 | } 385 | 386 | try self.metalContext.startTask(toRender: outputImage, from: outputImage.extent, to: destination, at: .zero) 387 | 388 | commandBuffer.present(currentDrawable) 389 | commandBuffer.commit() 390 | } 391 | catch 392 | { 393 | print("Failed to render frame with Metal.", error) 394 | } 395 | } 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /DeltaCore/Emulator Core/Audio/AudioManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioManager.swift 3 | // DeltaCore 4 | // 5 | // Created by Riley Testut on 1/12/16. 6 | // Copyright © 2016 Riley Testut. All rights reserved. 7 | // 8 | 9 | import AVFoundation 10 | 11 | internal extension AVAudioFormat 12 | { 13 | var frameSize: Int { 14 | return Int(self.streamDescription.pointee.mBytesPerFrame) 15 | } 16 | } 17 | 18 | private extension AVAudioSession 19 | { 20 | func setDeltaCategory() throws 21 | { 22 | try AVAudioSession.sharedInstance().setCategory(.playAndRecord, 23 | options: [.mixWithOthers, .allowBluetoothA2DP, .allowAirPlay]) 24 | } 25 | } 26 | 27 | private extension AVAudioSessionRouteDescription 28 | { 29 | var isHeadsetPluggedIn: Bool 30 | { 31 | let isHeadsetPluggedIn = self.outputs.contains { $0.portType == .headphones || $0.portType == .bluetoothA2DP } 32 | return isHeadsetPluggedIn 33 | } 34 | 35 | var isOutputtingToReceiver: Bool 36 | { 37 | let isOutputtingToReceiver = self.outputs.contains { $0.portType == .builtInReceiver } 38 | return isOutputtingToReceiver 39 | } 40 | 41 | var isOutputtingToExternalDevice: Bool 42 | { 43 | let isOutputtingToExternalDevice = self.outputs.contains { $0.portType != .builtInSpeaker && $0.portType != .builtInReceiver } 44 | return isOutputtingToExternalDevice 45 | } 46 | } 47 | 48 | public class AudioManager: NSObject, AudioRendering 49 | { 50 | /// Currently only supports 16-bit interleaved Linear PCM. 51 | public internal(set) var audioFormat: AVAudioFormat { 52 | didSet { 53 | self.resetAudioEngine() 54 | } 55 | } 56 | 57 | public var isEnabled = true { 58 | didSet 59 | { 60 | self.audioBuffer.isEnabled = self.isEnabled 61 | 62 | self.updateOutputVolume() 63 | 64 | do 65 | { 66 | if self.isEnabled 67 | { 68 | try self.audioEngine.start() 69 | } 70 | else 71 | { 72 | self.audioEngine.pause() 73 | } 74 | } 75 | catch 76 | { 77 | print(error) 78 | } 79 | 80 | self.audioBuffer.reset() 81 | } 82 | } 83 | 84 | public var respectsSilentMode: Bool = true { 85 | didSet { 86 | self.updateOutputVolume() 87 | } 88 | } 89 | 90 | public private(set) var audioBuffer: RingBuffer 91 | 92 | public internal(set) var rate = 1.0 { 93 | didSet { 94 | self.timePitchEffect.rate = Float(self.rate) 95 | } 96 | } 97 | 98 | var frameDuration: Double = (1.0 / 60.0) { 99 | didSet { 100 | guard self.audioEngine.isRunning else { return } 101 | self.resetAudioEngine() 102 | } 103 | } 104 | 105 | private let audioEngine: AVAudioEngine 106 | private let audioPlayerNode: AVAudioPlayerNode 107 | private let timePitchEffect: AVAudioUnitTimePitch 108 | 109 | @available(iOS 13.0, *) 110 | private var sourceNode: AVAudioSourceNode { 111 | get { 112 | if _sourceNode == nil 113 | { 114 | _sourceNode = self.makeSourceNode() 115 | } 116 | 117 | return _sourceNode as! AVAudioSourceNode 118 | } 119 | set { 120 | _sourceNode = newValue 121 | } 122 | } 123 | private var _sourceNode: Any! = nil 124 | 125 | private var audioConverter: AVAudioConverter? 126 | private var audioConverterRequiredFrameCount: AVAudioFrameCount? 127 | 128 | private let audioBufferCount = 3 129 | 130 | // Used to synchronize access to self.audioPlayerNode without causing deadlocks. 131 | private let renderingQueue = DispatchQueue(label: "com.rileytestut.Delta.AudioManager.renderingQueue") 132 | 133 | private var isMuted: Bool = false { 134 | didSet { 135 | self.updateOutputVolume() 136 | } 137 | } 138 | 139 | private let muteSwitchMonitor = DLTAMuteSwitchMonitor() 140 | 141 | public init(audioFormat: AVAudioFormat) 142 | { 143 | self.audioFormat = audioFormat 144 | 145 | // Temporary. Will be replaced with more accurate RingBuffer in resetAudioEngine(). 146 | self.audioBuffer = RingBuffer(preferredBufferSize: 4096)! 147 | 148 | do 149 | { 150 | // Set category before configuring AVAudioEngine to prevent pausing any currently playing audio from another app. 151 | try AVAudioSession.sharedInstance().setDeltaCategory() 152 | } 153 | catch 154 | { 155 | print(error) 156 | } 157 | 158 | self.audioEngine = AVAudioEngine() 159 | 160 | self.audioPlayerNode = AVAudioPlayerNode() 161 | self.audioEngine.attach(self.audioPlayerNode) 162 | 163 | self.timePitchEffect = AVAudioUnitTimePitch() 164 | self.audioEngine.attach(self.timePitchEffect) 165 | 166 | super.init() 167 | 168 | if #available(iOS 13.0, *) 169 | { 170 | self.audioEngine.attach(self.sourceNode) 171 | } 172 | 173 | self.updateOutputVolume() 174 | 175 | NotificationCenter.default.addObserver(self, selector: #selector(AudioManager.resetAudioEngine), name: .AVAudioEngineConfigurationChange, object: nil) 176 | NotificationCenter.default.addObserver(self, selector: #selector(AudioManager.resetAudioEngine), name: AVAudioSession.routeChangeNotification, object: nil) 177 | } 178 | } 179 | 180 | public extension AudioManager 181 | { 182 | func start() 183 | { 184 | self.muteSwitchMonitor.startMonitoring { [weak self] (isMuted) in 185 | self?.isMuted = isMuted 186 | } 187 | 188 | do 189 | { 190 | try AVAudioSession.sharedInstance().setDeltaCategory() 191 | try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005) 192 | 193 | if #available(iOS 13.0, *) 194 | { 195 | try AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true) 196 | } 197 | 198 | try AVAudioSession.sharedInstance().setActive(true) 199 | } 200 | catch 201 | { 202 | print(error) 203 | } 204 | 205 | self.resetAudioEngine() 206 | } 207 | 208 | func stop() 209 | { 210 | self.muteSwitchMonitor.stopMonitoring() 211 | 212 | self.renderingQueue.sync { 213 | self.audioPlayerNode.stop() 214 | self.audioEngine.stop() 215 | } 216 | 217 | self.audioBuffer.isEnabled = false 218 | } 219 | } 220 | 221 | private extension AudioManager 222 | { 223 | func render(_ inputBuffer: AVAudioPCMBuffer, into outputBuffer: AVAudioPCMBuffer) 224 | { 225 | guard let buffer = inputBuffer.int16ChannelData, let audioConverter = self.audioConverter else { return } 226 | 227 | // Ensure any buffers from previous audio route configurations are no longer processed. 228 | guard inputBuffer.format == audioConverter.inputFormat && outputBuffer.format == audioConverter.outputFormat else { return } 229 | 230 | if self.audioConverterRequiredFrameCount == nil 231 | { 232 | // Determine the minimum number of input frames needed to perform a conversion. 233 | audioConverter.convert(to: outputBuffer, error: nil) { (requiredPacketCount, outStatus) -> AVAudioBuffer? in 234 | // In Linear PCM, one packet = one frame. 235 | self.audioConverterRequiredFrameCount = requiredPacketCount 236 | 237 | // Setting to ".noDataNow" sometimes results in crash, so we set to ".endOfStream" and reset audioConverter afterwards. 238 | outStatus.pointee = .endOfStream 239 | return nil 240 | } 241 | 242 | audioConverter.reset() 243 | } 244 | 245 | guard let audioConverterRequiredFrameCount = self.audioConverterRequiredFrameCount else { return } 246 | 247 | let availableFrameCount = AVAudioFrameCount(self.audioBuffer.availableBytesForReading / self.audioFormat.frameSize) 248 | if self.audioEngine.isRunning && availableFrameCount >= audioConverterRequiredFrameCount 249 | { 250 | var conversionError: NSError? 251 | let status = audioConverter.convert(to: outputBuffer, error: &conversionError) { (requiredPacketCount, outStatus) -> AVAudioBuffer? in 252 | 253 | // Copy requiredPacketCount frames into inputBuffer's first channel (since audio is interleaved, no need to modify other channels). 254 | let preferredSize = min(Int(requiredPacketCount) * self.audioFormat.frameSize, Int(inputBuffer.frameCapacity) * self.audioFormat.frameSize) 255 | buffer[0].withMemoryRebound(to: UInt8.self, capacity: preferredSize) { (uint8Buffer) in 256 | let readBytes = self.audioBuffer.read(into: uint8Buffer, preferredSize: preferredSize) 257 | 258 | let frameLength = AVAudioFrameCount(readBytes / self.audioFormat.frameSize) 259 | inputBuffer.frameLength = frameLength 260 | } 261 | 262 | if inputBuffer.frameLength == 0 263 | { 264 | outStatus.pointee = .noDataNow 265 | return nil 266 | } 267 | else 268 | { 269 | outStatus.pointee = .haveData 270 | return inputBuffer 271 | } 272 | } 273 | 274 | if status == .error 275 | { 276 | if let error = conversionError 277 | { 278 | print(error, error.userInfo) 279 | } 280 | } 281 | } 282 | else 283 | { 284 | // If not running or not enough input frames, set frameLength to 0 to minimize time until we check again. 285 | inputBuffer.frameLength = 0 286 | } 287 | 288 | self.audioPlayerNode.scheduleBuffer(outputBuffer) { [weak self, weak node = audioPlayerNode] in 289 | guard let self = self else { return } 290 | 291 | self.renderingQueue.async { 292 | if node?.isPlaying == true 293 | { 294 | self.render(inputBuffer, into: outputBuffer) 295 | } 296 | } 297 | } 298 | } 299 | 300 | @objc func resetAudioEngine() 301 | { 302 | self.renderingQueue.sync { 303 | self.audioPlayerNode.reset() 304 | 305 | guard let outputAudioFormat = AVAudioFormat(standardFormatWithSampleRate: AVAudioSession.sharedInstance().sampleRate, channels: self.audioFormat.channelCount) else { return } 306 | 307 | let inputAudioBufferFrameCount = Int(self.audioFormat.sampleRate * self.frameDuration) 308 | let outputAudioBufferFrameCount = Int(outputAudioFormat.sampleRate * self.frameDuration) 309 | 310 | // Allocate enough space to prevent us from overwriting data before we've used it. 311 | let ringBufferAudioBufferCount = Int((self.audioFormat.sampleRate / outputAudioFormat.sampleRate).rounded(.up) + 10.0) 312 | 313 | let preferredBufferSize = inputAudioBufferFrameCount * self.audioFormat.frameSize * ringBufferAudioBufferCount 314 | guard let ringBuffer = RingBuffer(preferredBufferSize: preferredBufferSize) else { 315 | fatalError("Cannot initialize RingBuffer with preferredBufferSize of \(preferredBufferSize)") 316 | } 317 | self.audioBuffer = ringBuffer 318 | 319 | let audioConverter = AVAudioConverter(from: self.audioFormat, to: outputAudioFormat) 320 | self.audioConverter = audioConverter 321 | 322 | self.audioConverterRequiredFrameCount = nil 323 | 324 | self.audioEngine.disconnectNodeOutput(self.timePitchEffect) 325 | self.audioEngine.connect(self.timePitchEffect, to: self.audioEngine.mainMixerNode, format: outputAudioFormat) 326 | 327 | if #available(iOS 13.0, *) 328 | { 329 | self.audioEngine.detach(self.sourceNode) 330 | 331 | self.sourceNode = self.makeSourceNode() 332 | self.audioEngine.attach(self.sourceNode) 333 | 334 | self.audioEngine.connect(self.sourceNode, to: self.timePitchEffect, format: outputAudioFormat) 335 | } 336 | else 337 | { 338 | self.audioEngine.disconnectNodeOutput(self.audioPlayerNode) 339 | self.audioEngine.connect(self.audioPlayerNode, to: self.timePitchEffect, format: outputAudioFormat) 340 | 341 | for _ in 0 ..< self.audioBufferCount 342 | { 343 | let inputAudioBufferFrameCapacity = max(inputAudioBufferFrameCount, outputAudioBufferFrameCount) 344 | 345 | if let inputBuffer = AVAudioPCMBuffer(pcmFormat: self.audioFormat, frameCapacity: AVAudioFrameCount(inputAudioBufferFrameCapacity)), 346 | let outputBuffer = AVAudioPCMBuffer(pcmFormat: outputAudioFormat, frameCapacity: AVAudioFrameCount(outputAudioBufferFrameCount)) 347 | { 348 | self.render(inputBuffer, into: outputBuffer) 349 | } 350 | } 351 | } 352 | 353 | do 354 | { 355 | // Explicitly set output port since .defaultToSpeaker option pauses external audio. 356 | if AVAudioSession.sharedInstance().currentRoute.isOutputtingToReceiver 357 | { 358 | try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) 359 | } 360 | 361 | try self.audioEngine.start() 362 | 363 | if #available(iOS 13.0, *) {} 364 | else 365 | { 366 | self.audioPlayerNode.play() 367 | } 368 | } 369 | catch 370 | { 371 | print(error) 372 | } 373 | } 374 | } 375 | 376 | @objc func updateOutputVolume() 377 | { 378 | if !self.isEnabled 379 | { 380 | self.audioEngine.mainMixerNode.outputVolume = 0.0 381 | } 382 | else 383 | { 384 | let route = AVAudioSession.sharedInstance().currentRoute 385 | 386 | if AVAudioSession.sharedInstance().isOtherAudioPlaying 387 | { 388 | // Always mute if another app is playing audio. 389 | self.audioEngine.mainMixerNode.outputVolume = 0.0 390 | } 391 | else if self.respectsSilentMode 392 | { 393 | if self.isMuted && (route.isHeadsetPluggedIn || !route.isOutputtingToExternalDevice) 394 | { 395 | // Respect mute switch IFF playing through speaker or headphones. 396 | self.audioEngine.mainMixerNode.outputVolume = 0.0 397 | } 398 | else 399 | { 400 | // Ignore mute switch for other audio routes (e.g. AirPlay). 401 | self.audioEngine.mainMixerNode.outputVolume = 1.0 402 | } 403 | } 404 | else 405 | { 406 | // Ignore silent mode and always play game audio (unless another app is playing audio). 407 | self.audioEngine.mainMixerNode.outputVolume = 1.0 408 | } 409 | } 410 | } 411 | 412 | @available(iOS 13.0, *) 413 | func makeSourceNode() -> AVAudioSourceNode 414 | { 415 | var isPrimed = false 416 | var previousSampleCount: Int? 417 | 418 | // Accessing AVAudioSession.sharedInstance() from render block may cause audio glitches, 419 | // so calculate sampleRateRatio now rather than later when needed 🤷‍♂️ 420 | let sampleRateRatio = (self.audioFormat.sampleRate / AVAudioSession.sharedInstance().sampleRate).rounded(.up) 421 | 422 | let sourceNode = AVAudioSourceNode(format: self.audioFormat) { [audioFormat, audioBuffer] (_, _, frameCount, audioBufferList) -> OSStatus in 423 | defer { previousSampleCount = audioBuffer.availableBytesForReading } 424 | 425 | let unsafeAudioBufferList = UnsafeMutableAudioBufferListPointer(audioBufferList) 426 | guard let buffer = unsafeAudioBufferList[0].mData else { return kAudioFileStreamError_UnspecifiedError } 427 | 428 | let requestedBytes = Int(frameCount) * audioFormat.frameSize 429 | 430 | if !isPrimed 431 | { 432 | // Make sure audio buffer has enough initial samples to prevent audio distortion. 433 | 434 | guard audioBuffer.availableBytesForReading >= requestedBytes * Int(sampleRateRatio) else { return kAudioFileStreamError_DataUnavailable } 435 | isPrimed = true 436 | } 437 | 438 | if let previousSampleCount = previousSampleCount, audioBuffer.availableBytesForReading < previousSampleCount 439 | { 440 | // Audio buffer has been reset, so we need to prime it again. 441 | 442 | isPrimed = false 443 | return kAudioFileStreamError_DataUnavailable 444 | } 445 | 446 | guard audioBuffer.availableBytesForReading >= requestedBytes else { 447 | isPrimed = false 448 | return kAudioFileStreamError_DataUnavailable 449 | } 450 | 451 | let readBytes = audioBuffer.read(into: buffer, preferredSize: requestedBytes) 452 | unsafeAudioBufferList[0].mDataByteSize = UInt32(readBytes) 453 | 454 | return noErr 455 | } 456 | 457 | return sourceNode 458 | } 459 | } 460 | --------------------------------------------------------------------------------