diff --git a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift index 54bc1533c..566403846 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/NativeController.swift @@ -13,13 +13,29 @@ class NativeController: Hashable { private var controller: OpaquePointer? private var nativeController: GCController 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: 3.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 + } setupHandheldController() } @@ -55,7 +71,7 @@ class NativeController: Hashable { // 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 @@ -146,42 +162,6 @@ class NativeController: Hashable { } } - 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 da6a73a29..41d5d1c42 100644 --- a/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift +++ b/src/MeloNX/MeloNX/App/Core/Ryujinx/Controller/VirtualController.swift @@ -12,10 +12,28 @@ import UIKit class VirtualController { private var instanceID: SDL_JoystickID = -1 private var controller: OpaquePointer? + private let hapticEngine: CHHapticEngine? + private let rumbleController: RumbleController? 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() } @@ -46,7 +64,9 @@ class VirtualController { 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 }, @@ -78,52 +98,6 @@ class VirtualController { } } - 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 ecbba6c6b..c47737db5 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -1070,7 +1070,7 @@ namespace Ryujinx.Headless.SDL2 { StrongRumble = 1f, WeakRumble = 1f, - EnableRumble = false, + EnableRumble = true, }, }; }