Skip to content

Commit

Permalink
Merge branch 'feature/saveStatesSwiftUI' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeMatt committed Nov 26, 2024
2 parents f2ee974 + 0e43322 commit 20ce65e
Show file tree
Hide file tree
Showing 57 changed files with 4,685 additions and 220 deletions.
13 changes: 13 additions & 0 deletions PVEmulatorCore/Sources/PVEmulatorCore/PVEmulatorCore+RunLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,25 @@ import PVLogging
@objc open func setPauseEmulation(_ flag: Bool) {
if flag {
stopHaptic()
// Pause emulation loop
skipEmulationLoop = true
// Wait for current frame to complete
frontBufferLock.lock()
frontBufferLock.unlock()

isRunning = false
} else {
startHaptic()
// Resume emulation loop
skipEmulationLoop = false
shouldResyncTime = true
// // Signal render delegate to resume
// renderDelegate?.isPaused = false

isRunning = true
}
}


@objc open var isEmulationPaused: Bool { return !isRunning }

Expand Down
68 changes: 34 additions & 34 deletions PVEmulatorCore/Sources/PVEmulatorCore/PVEmulatorCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@ public typealias OptionalCore = PVEmulatorCore & CoreOptional
@objc
@objcMembers
open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {

open var bridge: (any ObjCBridgedCoreBridge)!

@MainActor
@objc public static var coreClassName: String = ""

@MainActor
@objc public static var systemName: String = ""

@objc dynamic open var resourceBundle: Bundle { Bundle.module }

@MainActor
@available(*, deprecated, message: "Why does this need to exist? Only used for macII in PVRetroCore")
public static var status: [String: Any] = .init()

// MARK: EmulatorCoreAudioDataSource

#if canImport(GameController)
@objc
public var valueChangedHandler: GCExtendedGamepadValueChangedHandler? = nil
Expand All @@ -57,7 +57,7 @@ open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {
startHaptic()
}
}

@objc dynamic open var controller2: GCController? {
get { bridge.controller2 }
set { bridge.controller2 = newValue } }
Expand All @@ -67,7 +67,7 @@ open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {
@objc dynamic open var controller4: GCController? {
get { bridge.controller4 }
set { bridge.controller4 = newValue } }

@objc dynamic open var controller5: GCController? {
get { bridge.controller5 }
set { bridge.controller5 = newValue } }
Expand All @@ -81,39 +81,39 @@ open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {
get { bridge.controller8 }
set { bridge.controller8 = newValue } }
#endif

#if !os(macOS) && !os(watchOS)
@objc open var touchViewController: UIViewController?
{ didSet { bridge.touchViewController = touchViewController} }
// { get{ bridge.touchViewController } set { bridge.touchViewController = newValue } }
#endif

// // MARK: EmulatorCoreRumbleDataSource
// var supportsRumble: Bool { bridge.supportsRumble }

// MARK: EmulatorCoreSavesDataSource

@objc dynamic open var batterySavesPath: String? = nil {
didSet { bridge.batterySavesPath = batterySavesPath }
}
@objc dynamic open var saveStatesPath: String? = nil {
didSet { bridge.saveStatesPath = saveStatesPath }
}

@objc dynamic open var supportsSaveStates: Bool { return bridge.supportsSaveStates ?? false }

// MARK: EmulatorCoreVideoDelegate

#if canImport(OpenGL) || canImport(OpenGLES)
@objc dynamic open var glesVersion: GLESVersion = .version3
#endif

// PVRenderDelegate
@objc open weak var renderDelegate: (any PVCoreBridge.PVRenderDelegate)?
{ get{ bridge.renderDelegate } set { bridge.renderDelegate = newValue } }

// MARK: EmulatorCoreRunLoop

/// Should stop
@objc dynamic open var shouldStop: Bool
{ get { bridge.shouldStop } set { bridge.shouldStop = newValue } }
Expand All @@ -125,16 +125,16 @@ open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {
{ get { bridge.skipEmulationLoop } set { bridge.skipEmulationLoop = newValue } }
@objc dynamic open var skipLayout: Bool
{ get { bridge.skipLayout } set { bridge.skipLayout = newValue } }

@available(*, deprecated, message: "What is this even used for?")
@objc dynamic open var isOn: Bool = false

@objc dynamic open var isFrontBufferReady: Bool
{ get { bridge.isFrontBufferReady } set { bridge.isFrontBufferReady = newValue } }

@objc dynamic open var gameSpeed: PVCoreBridge.GameSpeed = .normal
{ didSet { bridge.gameSpeed = gameSpeed }}

@objc dynamic open var emulationLoopThreadLock: NSLock
{ get { bridge.emulationLoopThreadLock } set { bridge.emulationLoopThreadLock = newValue } }

Expand All @@ -144,7 +144,7 @@ open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {
@objc dynamic open var frontBufferLock: NSLock
{ get { bridge.frontBufferLock } set { bridge.frontBufferLock = newValue } }


// MARK: EmulatorCoreIOInterface
@objc dynamic open var romName: String?
{ get { bridge.romName } set { bridge.romName = newValue } }
Expand All @@ -165,30 +165,30 @@ open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {
{ get { bridge.romSerial } set { bridge.romMD5 = romSerial } }

@objc dynamic open var discCount: UInt { bridge.discCount }

@objc dynamic open var screenType: ScreenTypeObjC = .crt

@objc dynamic open var extractArchive: Bool
{ get { bridge.extractArchive } set { bridge.extractArchive = newValue } }


// MARK: Audio
@objc dynamic open var audioDelegate: (any PVAudioDelegate)?
{ get { bridge.audioDelegate } set { bridge.audioDelegate = newValue } }

// MARK: Class

@objc open func initialize() {
// buildRingBuffers()
/// Fixes a race condition
bridge.touchViewController = touchViewController
bridge.initialize()

frontBufferLock = .init()
frontBufferCondition = .init()
emulationLoopThreadLock = .init()
}

// @nonobjc
// open func loadFile(atPath path: String) throws -> Bool {
// var error: NSError?
Expand All @@ -200,7 +200,7 @@ open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {
// }
// return success
// }

@objc(loadFileAtPath:error:)
open func loadFile(atPath path: String) throws {
// if let bridge = self as? ObjCCoreBridge {
Expand All @@ -209,20 +209,20 @@ open class PVEmulatorCore: NSObject, ObjCBridgedCore, PVEmulatorCoreT {
// }
throw EmulationError.coreDoesNotImplimentLoadFile
}

@objc
required public override init() {
super.init()
}

// private func buildRingBuffers() {
// let audioBufferCount = Int(audioBufferCount)
// ringBuffers = (0..<audioBufferCount).compactMap {
// let length: Int = Int(audioBufferSize(forBuffer: UInt($0)))
// return RingBuffer.init(withLength: length)
// }
// }

// EmulatorCoreAudioDataSource
@objc dynamic open var ringBuffers: [RingBufferProtocol]?
{ get { bridge.ringBuffers } set { bridge.ringBuffers = newValue }}
Expand Down
9 changes: 6 additions & 3 deletions PVLibrary/Sources/PVFileSystem/Paths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ public extension URL {
#if os(tvOS)
return cachesPath
#else
// let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
// return URL(fileURLWithPath: paths.first!, isDirectory: true)
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: PVAppGroupId)!.appending(component: "Documents/")
if let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: PVAppGroupId) {
return groupURL.appendingPathComponent("Documents/")
} else {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
return URL(fileURLWithPath: paths.first!, isDirectory: true)
}
#endif
}()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import AsyncAlgorithms
import Systems
import PVMediaCache

let schemaVersion: UInt64 = 13
let schemaVersion: UInt64 = 14

public enum RomDeletionError: Error {
case relatedFiledDeletionError
Expand Down Expand Up @@ -159,6 +159,12 @@ public final class RealmConfiguration {
newObject!["appStoreDisabled"] = false
}
}
if oldSchemaVersion < 14 {
migration.enumerateObjects(ofType: PVSaveState.className()) { oldObject, newObject in
newObject!["isPinned"] = false
newObject!["isFavorite"] = false
}
}
}

#if DEBUG
Expand Down Expand Up @@ -467,6 +473,35 @@ public extension RomDatabase {
func allGamesSortedBySystemThenTitle() -> Results<PVGame> {
return realm.objects(PVGame.self).sorted(byKeyPath: "systemIdentifier").sorted(byKeyPath: "title")
}

// MARK: Save States

func allSaveStates() -> Results<PVSaveState> {
return all(PVSaveState.self)
}

func allSaveStates(forGameWithID gameID: String) -> Results<PVSaveState> {
let game = realm.object(ofType: PVGame.self, forPrimaryKey: gameID)
return realm.objects(PVSaveState.self).filter("game == %@", game)
}

func savetate(forID saveStateID: String) -> PVSaveState? {
if let saveState = realm.object(ofType: PVSaveState.self, forPrimaryKey: saveStateID) {
return saveState
} else {
return nil
}
}
}

public extension Object {
public static func all() -> Results<PersistedType> {
try! Realm().objects(Self.PersistedType)
}

public static func forPrimaryKey(_ primaryKey: String) -> PersistedType? {
try! Realm().object(ofType: Self.PersistedType.self, forPrimaryKey: primaryKey)
}
}

// MARK: - Update
Expand Down
68 changes: 42 additions & 26 deletions PVLibrary/Sources/PVMediaCache/PVMediaCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,40 +268,56 @@ public final class PVMediaCache: NSObject, Sendable {
#else
public typealias ImageFetchCompletion = @Sendable (_ key: String, _ image: UIImage?) -> Void

@discardableResult
public func image(forKey key: String, completion: ImageFetchCompletion? = nil) -> BlockOperation? {
DLOG("Attempting to fetch image for key: \(key)")
/// Async version of image fetching
public func image(forKey key: String) async -> UIImage? {
guard !key.isEmpty else {
DLOG("Error: Key was empty")
DispatchQueue.main.async {
completion?(key, nil)
}
return nil
}

DLOG("Attempting to fetch image for key: \(key)")
let keyHash = key.md5Hash
let cacheDir = PVMediaCache.cachePath
let cachePath = cacheDir.appendingPathComponent(keyHash, isDirectory: false).path
DLOG("Cache path for key: \(cachePath)")

// Check memory cache first
if let cachedImage = await MainActor.run(body: {
PVMediaCache.memCache.object(forKey: keyHash as NSString)
}) {
DLOG("Image found in memory cache")
return cachedImage
}

// Check disk cache
guard FileManager.default.fileExists(atPath: cachePath) else {
DLOG("Image not found on disk")
return nil
}

DLOG("Attempting to load image from disk")
guard let image = UIImage(contentsOfFile: cachePath) else {
DLOG("Failed to load image from disk")
return nil
}

// Store in memory cache
await MainActor.run {
PVMediaCache.memCache.setObject(image, forKey: keyHash as NSString)
DLOG("Image added to memory cache")
}

return image
}

/// Legacy completion handler version that internally uses the async version
@discardableResult
public func image(forKey key: String, completion: ImageFetchCompletion? = nil) -> BlockOperation? {
let operation = BlockOperation { [weak self] in
guard let self = self else { return }
let cacheDir = PVMediaCache.cachePath
let keyHash = key.md5Hash
let cachePath = cacheDir.appendingPathComponent(keyHash, isDirectory: false).path
DLOG("Cache path for key: \(cachePath)")

Task { @MainActor in
var image: UIImage?
image = PVMediaCache.memCache.object(forKey: keyHash as NSString)
DLOG("Image found in memory cache: \(image != nil)")

if image == nil, FileManager.default.fileExists(atPath: cachePath) {
DLOG("Attempting to load image from disk")
image = UIImage(contentsOfFile: cachePath)
DLOG("Image loaded from disk: \(image != nil)")

if let image = image {
PVMediaCache.memCache.setObject(image, forKey: keyHash as NSString)
DLOG("Image added to memory cache")
}
}

Task {
let image = await self.image(forKey: key)
DispatchQueue.main.async {
completion?(key, image)
}
Expand Down
Loading

0 comments on commit 20ce65e

Please sign in to comment.