diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift index f6853b548..a1a67d643 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift @@ -14,13 +14,29 @@ class NativeController: Hashable, BaseController { private var nativeController: GCController private var controllerMotionProvider: ControllerMotionProvider? private let controllerHaptics: CHHapticEngine? + private let rumbleController: RumbleController? public var controllername: String { "GC - \(nativeController.vendorName ?? "Unknown")" } init(_ controller: GCController) { nativeController = controller controllerHaptics = nativeController.haptics?.createEngine(withLocality: .default) - try? controllerHaptics?.start() + + // Make sure the haptic engine exists before attempting to start it or initialize the controller. + if let hapticsEngine = controllerHaptics { + do { + try hapticsEngine.start() + rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 2.5) + + // print("CHHapticEngine started and RumbleController initialized.") + } catch { + // print("Error starting CHHapticEngine: \(error.localizedDescription)") + rumbleController = nil + } + } else { + // print("CHHapticEngine is nil. Cannot initialize RumbleController.") + rumbleController = nil + } setupHandheldController() } @@ -70,7 +86,7 @@ class NativeController: Hashable, BaseController { // print("Rumble with \(lowFreq), \(highFreq)") guard let userdata else { return 0 } let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue() - VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq), engine: _self.controllerHaptics) + _self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq)) return 0 }, RumbleTriggers: { userdata, leftRumble, rightRumble in @@ -161,42 +177,6 @@ class NativeController: Hashable, BaseController { } } - static func rumble(lowFreq: Float, highFreq: Float) { - do { - // Low-frequency haptic pattern - let lowFreqPattern = try CHHapticPattern(events: [ - CHHapticEvent(eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) - ], relativeTime: 0, duration: 0.2) - ], parameters: []) - - // High-frequency haptic pattern - let highFreqPattern = try CHHapticPattern(events: [ - CHHapticEvent(eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) - ], relativeTime: 0.2, duration: 0.2) - ], parameters: []) - - // Create and start the haptic engine - let engine = try CHHapticEngine() - try engine.start() - - // Create and play the low-frequency player - let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern) - try lowFreqPlayer.start(atTime: 0) - - // Create and play the high-frequency player after a short delay - let highFreqPlayer = try engine.makePlayer(with: highFreqPattern) - try highFreqPlayer.start(atTime: 0.2) - - } catch { - // print("Error creating haptic patterns: \(error)") - } - } - - func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) { guard controller != nil else { return } let joystick = SDL_JoystickFromInstanceID(instanceID) diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift new file mode 100644 index 000000000..d6718fce1 --- /dev/null +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/Rumble/RumbleController.swift @@ -0,0 +1,132 @@ +// +// RumbleController.swift +// MeloNX +// +// Created by MediaMoots on 2025/5/24. +// + +import CoreHaptics +import Foundation + +class RumbleController { + + private var engine: CHHapticEngine? + private var lowHapticPlayer: CHHapticPatternPlayer? + private var highHapticPlayer: CHHapticPatternPlayer? + private var rumbleMultiplier: Float = 1.0 + + // The duration of each continuous haptic event. + // We'll restart the players before this duration expires. + private let hapticEventDuration: TimeInterval = 7200 + + // Timer to schedule player restarts + private var playerRestartTimer: Timer? + + // Interval before the haptic event duration runs out to restart + private let restartGracePeriod: TimeInterval = 1.0 + + init (engine: CHHapticEngine?, rumbleMultiplier: Float) { + self.engine = engine + self.rumbleMultiplier = rumbleMultiplier + + createPlayers() + setupPlayerRestartTimer() + } + + // Deinitializer to clean up the timer and stop players when the controller is deallocated + deinit { + playerRestartTimer?.invalidate() // Stop the timer + playerRestartTimer = nil + + // Optionally stop the haptic players immediately + try? lowHapticPlayer?.stop(atTime: CHHapticTimeImmediate) + try? highHapticPlayer?.stop(atTime: CHHapticTimeImmediate) + + // print("RumbleController deinitialized.") + } + + // MARK: - Private Methods for Player Management + private func createPlayers() { + // Ensure the engine is available before proceeding + guard let engine = self.engine else { + // print("CHHapticEngine is nil. Cannot initialize RumbleController.") + return + } + + do { + let baseIntensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0) + + let lowSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.0) + let highSharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1) + + // Create continuous haptic events with the defined duration + let lowContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, lowSharpness], relativeTime: 0, duration: hapticEventDuration) + let highContinuousEvent = CHHapticEvent(eventType: .hapticContinuous, parameters: [baseIntensity, highSharpness], relativeTime: 0, duration: hapticEventDuration) + + // Create patterns from the continuous haptic events. + let lowPattern = try CHHapticPattern(events: [lowContinuousEvent], parameters: []) + let highPattern = try CHHapticPattern(events: [highContinuousEvent], parameters: []) + + // Make players from the patterns + lowHapticPlayer = try engine.makePlayer(with: lowPattern) + highHapticPlayer = try engine.makePlayer(with: highPattern) + + rumble(lowFreq: 0, highFreq: 0) + + // Start players initially + try lowHapticPlayer?.start(atTime: 0) + try highHapticPlayer?.start(atTime: 0) + } catch { + // print("Error initializing RumbleController or setting up haptic player: \(error.localizedDescription)") + + // Clean up if setup fails + lowHapticPlayer = nil + highHapticPlayer = nil + playerRestartTimer?.invalidate() + playerRestartTimer = nil + } + } + + private func setupPlayerRestartTimer() { + // Invalidate any existing timer to prevent multiple timers if init is called multiple times + playerRestartTimer?.invalidate() + + // Calculate the interval for restarting: 1 second before the haptic event duration ends + let restartInterval = hapticEventDuration - restartGracePeriod + + guard restartInterval > 0 else { + // print("Warning: hapticEventDuration (\(hapticEventDuration)s) is too short for scheduled restart with grace period (\(restartGracePeriod)s). Timer will not be set.") + return + } + + // Schedule a repeating timer that calls restartPlayers() + playerRestartTimer = Timer.scheduledTimer(withTimeInterval: restartInterval, repeats: true) { [weak self] _ in + self?.createPlayers() + } + // Ensure the timer is added to the current run loop in its default mode + RunLoop.current.add(playerRestartTimer!, forMode: .default) + + // print("Haptic Players restart timer scheduled to fire every \(restartInterval) seconds.") + } + + // MARK: - Public Rumble Control + + public func rumble(lowFreq: Float, highFreq: Float) { + + // Normalize SDL values (0-65535) to CoreHaptics range (0.0-1.0) + let normalizedLow = min(1.0, max(0.0, lowFreq * rumbleMultiplier / 65535.0)) + let normalizedHigh = min(1.0, max(0.0, highFreq * rumbleMultiplier / 65535.0)) + + // Create dynamic parameters to control intensity + let lowIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedLow, relativeTime: 0) + let highIntensityParameter = CHHapticDynamicParameter(parameterID: .hapticIntensityControl, value: normalizedHigh, relativeTime: 0) + + // Send parameters to the players + do { + try lowHapticPlayer?.sendParameters([lowIntensityParameter], atTime: 0) + try highHapticPlayer?.sendParameters([highIntensityParameter], atTime: 0) + } catch { + // print("Error sending haptic parameters: \(error.localizedDescription)") + } + } +} diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift index f4f278945..9ffbcf85d 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift @@ -12,11 +12,29 @@ import UIKit class VirtualController : BaseController { private var instanceID: SDL_JoystickID = -1 private var controller: OpaquePointer? + private let hapticEngine: CHHapticEngine? + private let rumbleController: RumbleController? private var deviceMotionProvider: DeviceMotionProvider? public let controllername = "MeloNX Touch Controller" init() { + // Setup Haptics + hapticEngine = try? CHHapticEngine() + if let hapticsEngine = hapticEngine { + do { + try hapticsEngine.start() + rumbleController = RumbleController(engine: hapticsEngine, rumbleMultiplier: 2.0) + + // print("CHHapticEngine started and RumbleController initialized.") + } catch { + // print("Error starting CHHapticEngine: \(error.localizedDescription)") + rumbleController = nil + } + } else { + // print("CHHapticEngine is nil. Cannot initialize RumbleController.") + rumbleController = nil + } setupVirtualController() } @@ -51,7 +69,7 @@ class VirtualController : BaseController { button_mask: 0, axis_mask: 0, name: controllername.withCString { $0 }, - userdata: nil, + userdata: Unmanaged.passUnretained(self).toOpaque(), Update: { userdata in // Update joystick state here }, @@ -61,7 +79,9 @@ class VirtualController : BaseController { Rumble: { userdata, lowFreq, highFreq in // print("Rumble with \(lowFreq), \(highFreq)") if UIDevice.current.userInterfaceIdiom == .phone { - VirtualController.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq)) + guard let userdata else { return 0 } + let _self = Unmanaged.fromOpaque(userdata).takeUnretainedValue() + _self.rumbleController?.rumble(lowFreq: Float(lowFreq), highFreq: Float(highFreq)) } return 0 }, @@ -93,52 +113,6 @@ class VirtualController : BaseController { } } - static func rumble(lowFreq: Float, highFreq: Float, engine: CHHapticEngine? = nil) { - do { - let lowFreqPattern = try CHHapticPattern(events: [ - CHHapticEvent(eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: lowFreq), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) - ], relativeTime: 0, duration: 0.2) - ], parameters: []) - - - let highFreqPattern = try CHHapticPattern(events: [ - CHHapticEvent(eventType: .hapticTransient, parameters: [ - CHHapticEventParameter(parameterID: .hapticIntensity, value: highFreq), - CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) - ], relativeTime: 0.2, duration: 0.2) - ], parameters: []) - - var engine = engine - - if engine == nil { - if hapticEngine == nil { - hapticEngine = try CHHapticEngine() - try hapticEngine?.start() - } - - engine = hapticEngine - } - - guard let engine else { - return // print("Error creating haptic patterns: hapticEngine is nil") - } - - let lowFreqPlayer = try engine.makePlayer(with: lowFreqPattern) - try lowFreqPlayer.start(atTime: 0) - - let highFreqPlayer = try engine.makePlayer(with: highFreqPattern) - try highFreqPlayer.start(atTime: 0) - - } catch { - // print("Error creating haptic patterns: \(error)") - } - } - - private static var hapticEngine: CHHapticEngine? - - func updateAxisValue(value: Sint16, forAxis axis: SDL_GameControllerAxis) { guard controller != nil else { return } let joystick = SDL_JoystickFromInstanceID(instanceID) diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index b47c50246..b1e01caa0 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -1071,7 +1071,7 @@ namespace Ryujinx.Headless.SDL2 { StrongRumble = 1f, WeakRumble = 1f, - EnableRumble = false, + EnableRumble = true, }, };