├── 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 |
--------------------------------------------------------------------------------