Skip to content

Commit

Permalink
Gamepad.vibrationActuator: Add support for "trigger-rumble" effect type
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=250352
rdar://104315486

Reviewed by Brent Fulgham and Geoffrey Garen.

Gamepad.vibrationActuator: Add support for "trigger-rumble" effect type:
-  https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/GamepadHapticsActuatorTriggerRumble/explainer.md

It allows vibrating the triggers (which the XBox controller supports), while the
existing "dual-rumble" only makes the handles vibrate.

This isn't yet part of the specification at:
- https://w3c.github.io/gamepad/extensions.html#dom-gamepadhapticeffecttype

However, it is supported by Blink and used by XBox cloud games.

I am adding support behind an experimental feature flag, off by default.

* Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml:
* Source/WebCore/Modules/gamepad/GamepadEffectParameters.h:
* Source/WebCore/Modules/gamepad/GamepadEffectParameters.idl:
* Source/WebCore/Modules/gamepad/GamepadHapticActuator.cpp:
(WebCore::GamepadHapticActuator::canPlayEffectType const):
(WebCore::GamepadHapticActuator::playEffect):
(WebCore::GamepadHapticActuator::stopEffects):
(WebCore::GamepadHapticActuator::document const):
(WebCore::GamepadHapticActuator::promiseForEffectType):
* Source/WebCore/Modules/gamepad/GamepadHapticActuator.h:
* Source/WebCore/Modules/gamepad/GamepadHapticEffectType.idl:
* Source/WebCore/platform/gamepad/cocoa/GameControllerGamepad.mm:
(WebCore::GameControllerGamepad::setupElements):
* Source/WebCore/platform/gamepad/cocoa/GameControllerHapticEffect.h:
* Source/WebCore/platform/gamepad/cocoa/GameControllerHapticEffect.mm:
(WebCore::GameControllerHapticEffect::create):
(WebCore::GameControllerHapticEffect::GameControllerHapticEffect):
(WebCore::GameControllerHapticEffect::start):
(WebCore::GameControllerHapticEffect::stop):
(WebCore::GameControllerHapticEffect::leftEffectFinishedPlaying):
(WebCore::GameControllerHapticEffect::rightEffectFinishedPlaying):
(WebCore::GameControllerHapticEffect::strongEffectFinishedPlaying): Deleted.
(WebCore::GameControllerHapticEffect::weakEffectFinishedPlaying): Deleted.
* Source/WebCore/platform/gamepad/cocoa/GameControllerHapticEngines.h:
(WebCore::GameControllerHapticEngines::leftHandleEngine):
(WebCore::GameControllerHapticEngines::rightHandleEngine):
(WebCore::GameControllerHapticEngines::leftTriggerEngine):
(WebCore::GameControllerHapticEngines::rightTriggerEngine):
(WebCore::GameControllerHapticEngines::strongEngine): Deleted.
(WebCore::GameControllerHapticEngines::weakEngine): Deleted.
* Source/WebCore/platform/gamepad/cocoa/GameControllerHapticEngines.mm:
(WebCore::GameControllerHapticEngines::GameControllerHapticEngines):
(WebCore::GameControllerHapticEngines::currentEffectForType):
(WebCore::GameControllerHapticEngines::playEffect):
(WebCore::GameControllerHapticEngines::stopEffects):
(WebCore::GameControllerHapticEngines::ensureStarted):
(WebCore::GameControllerHapticEngines::stop):
* Source/WebCore/platform/gamepad/cocoa/GameControllerSoftLink.h:
* Source/WebCore/platform/gamepad/cocoa/GameControllerSoftLink.mm:
* Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in:

Canonical link: https://commits.webkit.org/259507@main
  • Loading branch information
cdumez committed Jan 27, 2023
1 parent fc0d8fb commit 2dca512
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 89 deletions.
14 changes: 14 additions & 0 deletions Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml
Expand Up @@ -2405,6 +2405,20 @@ GStreamerEnabled:
WebKit:
default: true

GamepadTriggerRumbleEnabled:
type: bool
status: testable
humanReadableName: "Gamepad trigger vibration support"
humanReadableDescription: "Support for Gamepad trigger vibration"
condition: ENABLE(GAMEPAD)
defaultValue:
WebKitLegacy:
default: false
WebKit:
default: false
WebCore:
default: false

GamepadVibrationActuatorEnabled:
type: bool
status: testable
Expand Down
3 changes: 3 additions & 0 deletions Source/WebCore/Modules/gamepad/GamepadEffectParameters.h
Expand Up @@ -37,6 +37,9 @@ struct GamepadEffectParameters {
double strongMagnitude = 0.0;
double weakMagnitude = 0.0;

double leftTrigger = 0.0;
double rightTrigger = 0.0;

// A maximum duration of 5 seconds is recommended by the specification:
// - https://w3c.github.io/gamepad/extensions.html#gamepadeffectparameters-dictionary
static constexpr Seconds maximumDuration = 5_s;
Expand Down
5 changes: 5 additions & 0 deletions Source/WebCore/Modules/gamepad/GamepadEffectParameters.idl
Expand Up @@ -31,4 +31,9 @@
double startDelay = 0.0;
double strongMagnitude = 0.0;
double weakMagnitude = 0.0;

// Not in the specification but supported by Blink:
// https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/GamepadHapticsActuatorTriggerRumble/explainer.md
double leftTrigger = 0.0;
double rightTrigger = 0.0;
};
46 changes: 38 additions & 8 deletions Source/WebCore/Modules/gamepad/GamepadHapticActuator.cpp
Expand Up @@ -47,6 +47,10 @@ static bool areEffectParametersValid(GamepadHapticEffectType effectType, const G
if (parameters.weakMagnitude < 0 || parameters.strongMagnitude < 0 || parameters.weakMagnitude > 1 || parameters.strongMagnitude > 1)
return false;
}
if (effectType == GamepadHapticEffectType::TriggerRumble) {
if (parameters.leftTrigger < 0 || parameters.rightTrigger < 0 || parameters.leftTrigger > 1 || parameters.rightTrigger > 1)
return false;
}
return true;
}

Expand All @@ -70,6 +74,9 @@ GamepadHapticActuator::~GamepadHapticActuator() = default;

bool GamepadHapticActuator::canPlayEffectType(EffectType effectType) const
{
if (effectType == EffectType::TriggerRumble && (!document() || !document()->settings().gamepadTriggerRumbleEnabled()))
return false;

return m_gamepad && m_gamepad->supportedEffectTypes().contains(effectType);
}

Expand All @@ -85,7 +92,8 @@ void GamepadHapticActuator::playEffect(EffectType effectType, GamepadEffectParam
promise->resolve<IDLEnumeration<Result>>(Result::Preempted);
return;
}
if (auto playingEffectPromise = std::exchange(m_playingEffectPromise, nullptr)) {
auto& currentEffectPromise = promiseForEffectType(effectType);
if (auto playingEffectPromise = std::exchange(currentEffectPromise, nullptr)) {
queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [playingEffectPromise = WTFMove(playingEffectPromise)] {
playingEffectPromise->resolve<IDLEnumeration<Result>>(Result::Preempted);
});
Expand All @@ -97,11 +105,12 @@ void GamepadHapticActuator::playEffect(EffectType effectType, GamepadEffectParam

effectParameters.duration = std::min(effectParameters.duration, GamepadEffectParameters::maximumDuration.milliseconds());

m_playingEffectPromise = WTFMove(promise);
GamepadProvider::singleton().playEffect(m_gamepad->index(), m_gamepad->id(), effectType, effectParameters, [this, protectedThis = makePendingActivity(*this), playingEffectPromise = m_playingEffectPromise](bool success) mutable {
if (m_playingEffectPromise != playingEffectPromise)
currentEffectPromise = WTFMove(promise);
GamepadProvider::singleton().playEffect(m_gamepad->index(), m_gamepad->id(), effectType, effectParameters, [this, protectedThis = makePendingActivity(*this), playingEffectPromise = currentEffectPromise, effectType](bool success) mutable {
auto& currentEffectPromise = promiseForEffectType(effectType);
if (playingEffectPromise != currentEffectPromise)
return; // Was already pre-empted.
queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [playingEffectPromise = std::exchange(m_playingEffectPromise, nullptr), success] {
queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [playingEffectPromise = std::exchange(currentEffectPromise, nullptr), success] {
playingEffectPromise->resolve<IDLEnumeration<Result>>(success ? Result::Complete : Result::Preempted);
});
});
Expand All @@ -123,11 +132,16 @@ void GamepadHapticActuator::reset(Ref<DeferredPromise>&& promise)

void GamepadHapticActuator::stopEffects(CompletionHandler<void()>&& completionHandler)
{
if (!m_playingEffectPromise)
if (!m_triggerRumbleEffectPromise && !m_dualRumbleEffectPromise)
return completionHandler();

queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [playingEffectPromise = std::exchange(m_playingEffectPromise, nullptr)] {
playingEffectPromise->resolve<IDLEnumeration<Result>>(Result::Preempted);
auto dualRumbleEffectPromise = std::exchange(m_dualRumbleEffectPromise, nullptr);
auto triggerRumbleEffectPromise = std::exchange(m_triggerRumbleEffectPromise, nullptr);
queueTaskKeepingObjectAlive(*this, TaskSource::Gamepad, [dualRumbleEffectPromise = WTFMove(dualRumbleEffectPromise), triggerRumbleEffectPromise = WTFMove(triggerRumbleEffectPromise)] {
if (dualRumbleEffectPromise)
dualRumbleEffectPromise->resolve<IDLEnumeration<Result>>(Result::Preempted);
if (triggerRumbleEffectPromise)
triggerRumbleEffectPromise->resolve<IDLEnumeration<Result>>(Result::Preempted);
});
GamepadProvider::singleton().stopEffects(m_gamepad->index(), m_gamepad->id(), WTFMove(completionHandler));
}
Expand All @@ -137,6 +151,11 @@ Document* GamepadHapticActuator::document()
return downcast<Document>(scriptExecutionContext());
}

const Document* GamepadHapticActuator::document() const
{
return downcast<Document>(scriptExecutionContext());
}

const char* GamepadHapticActuator::activeDOMObjectName() const
{
return "GamepadHapticActuator";
Expand All @@ -160,6 +179,17 @@ void GamepadHapticActuator::visibilityStateChanged()
stopEffects([] { });
}

RefPtr<DeferredPromise>& GamepadHapticActuator::promiseForEffectType(EffectType effectType)
{
switch (effectType) {
case EffectType::TriggerRumble:
return m_triggerRumbleEffectPromise;
case EffectType::DualRumble:
break;
}
return m_dualRumbleEffectPromise;
}

} // namespace WebCore

#endif // ENABLE(GAMEPAD)
6 changes: 5 additions & 1 deletion Source/WebCore/Modules/gamepad/GamepadHapticActuator.h
Expand Up @@ -59,7 +59,10 @@ class GamepadHapticActuator : public RefCounted<GamepadHapticActuator>, public A
GamepadHapticActuator(Document*, Type, Gamepad&);

Document* document();
const Document* document() const;

void stopEffects(CompletionHandler<void()>&&);
RefPtr<DeferredPromise>& promiseForEffectType(EffectType);

// ActiveDOMObject.
const char* activeDOMObjectName() const final;
Expand All @@ -71,7 +74,8 @@ class GamepadHapticActuator : public RefCounted<GamepadHapticActuator>, public A

Type m_type;
WeakPtr<Gamepad> m_gamepad;
RefPtr<DeferredPromise> m_playingEffectPromise;
RefPtr<DeferredPromise> m_dualRumbleEffectPromise;
RefPtr<DeferredPromise> m_triggerRumbleEffectPromise;
};

} // namespace WebCore
Expand Down
4 changes: 3 additions & 1 deletion Source/WebCore/Modules/gamepad/GamepadHapticEffectType.idl
Expand Up @@ -27,5 +27,7 @@
Conditional=GAMEPAD
] enum GamepadHapticEffectType {
"dual-rumble",
"trigger-rumble", // Not in the specification but supported by Blink.
// Not in the specification but supported by Blink:
// https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/GamepadHapticsActuatorTriggerRumble/explainer.md
"trigger-rumble",
};
Expand Up @@ -76,6 +76,10 @@ static void disableDefaultSystemAction(GCControllerButtonInput *button)
if ([haptics.supportedLocalities containsObject:get_GameController_GCHapticsLocalityLeftHandle()] && [haptics.supportedLocalities containsObject:get_GameController_GCHapticsLocalityRightHandle()])
m_supportedEffectTypes.add(GamepadHapticEffectType::DualRumble);
}
if (canLoad_GameController_GCHapticsLocalityLeftTrigger() && canLoad_GameController_GCHapticsLocalityRightTrigger()) {
if ([haptics.supportedLocalities containsObject:get_GameController_GCHapticsLocalityLeftTrigger()] && [haptics.supportedLocalities containsObject:get_GameController_GCHapticsLocalityRightTrigger()])
m_supportedEffectTypes.add(GamepadHapticEffectType::TriggerRumble);
}
}
#endif

Expand Down
18 changes: 10 additions & 8 deletions Source/WebCore/platform/gamepad/cocoa/GameControllerHapticEffect.h
Expand Up @@ -29,29 +29,31 @@

#import <wtf/CompletionHandler.h>
#import <wtf/RetainPtr.h>
#import <wtf/WeakPtr.h>

namespace WebCore {

class GameControllerHapticEngines;
struct GamepadEffectParameters;
enum class GamepadHapticEffectType : uint8_t;

class GameControllerHapticEffect {
class GameControllerHapticEffect : public CanMakeWeakPtr<GameControllerHapticEffect> {
WTF_MAKE_FAST_ALLOCATED;
public:
static std::unique_ptr<GameControllerHapticEffect> create(GameControllerHapticEngines&, const GamepadEffectParameters&, CompletionHandler<void(bool)>&&);
static std::unique_ptr<GameControllerHapticEffect> create(GameControllerHapticEngines&, GamepadHapticEffectType, const GamepadEffectParameters&);
~GameControllerHapticEffect();

bool start();
void start(CompletionHandler<void(bool)>&&);
void stop();

void strongEffectFinishedPlaying();
void weakEffectFinishedPlaying();
void leftEffectFinishedPlaying();
void rightEffectFinishedPlaying();

private:
GameControllerHapticEffect(RetainPtr<id>&& strongPlayer, RetainPtr<id>&& weakPlayer, CompletionHandler<void(bool)>&&);
GameControllerHapticEffect(RetainPtr<id>&& leftPlayer, RetainPtr<id>&& rightPlayer);

RetainPtr<id> m_strongPlayer;
RetainPtr<id> m_weakPlayer;
RetainPtr<id> m_leftPlayer;
RetainPtr<id> m_rightPlayer;
CompletionHandler<void(bool)> m_completionHandler;
};

Expand Down
65 changes: 41 additions & 24 deletions Source/WebCore/platform/gamepad/cocoa/GameControllerHapticEffect.mm
Expand Up @@ -30,6 +30,7 @@

#import "GameControllerHapticEngines.h"
#import "GamepadEffectParameters.h"
#import "GamepadHapticEffectType.h"
#import "Logging.h"
#import <cmath>

Expand All @@ -45,7 +46,7 @@ static double magnitudeToIntensity(double magnitude)
return std::sqrt(std::clamp<double>(magnitude, 0, 1));
}

std::unique_ptr<GameControllerHapticEffect> GameControllerHapticEffect::create(GameControllerHapticEngines& engines, const GamepadEffectParameters& parameters, CompletionHandler<void(bool)>&& completionHandler)
std::unique_ptr<GameControllerHapticEffect> GameControllerHapticEffect::create(GameControllerHapticEngines& engines, GamepadHapticEffectType type, const GamepadEffectParameters& parameters)
{
auto createPlayer = [&](CHHapticEngine *engine, double magnitude) -> RetainPtr<id> {
NSDictionary* hapticDict = @{
Expand All @@ -71,20 +72,32 @@ static double magnitudeToIntensity(double magnitude)

return retainPtr([engine createPlayerWithPattern:pattern.get() error:&error]);
};
auto strongPlayer = createPlayer(engines.strongEngine(), parameters.strongMagnitude);
auto weakPlayer = createPlayer(engines.weakEngine(), parameters.weakMagnitude);
if (!strongPlayer || !weakPlayer) {

RetainPtr<id> leftPlayer;
RetainPtr<id> rightPlayer;
switch (type) {
case GamepadHapticEffectType::DualRumble: {
leftPlayer = createPlayer(engines.leftHandleEngine(), parameters.strongMagnitude);
rightPlayer = createPlayer(engines.rightHandleEngine(), parameters.weakMagnitude);
break;
}
case GamepadHapticEffectType::TriggerRumble: {
leftPlayer = createPlayer(engines.leftTriggerEngine(), parameters.leftTrigger);
rightPlayer = createPlayer(engines.rightTriggerEngine(), parameters.rightTrigger);
break;
}
}

if (!leftPlayer || !rightPlayer) {
RELEASE_LOG_ERROR(Gamepad, "GameControllerHapticEffect: Failed to create the haptic effect players");
completionHandler(false);
return nullptr;
}
return std::unique_ptr<GameControllerHapticEffect>(new GameControllerHapticEffect(WTFMove(strongPlayer), WTFMove(weakPlayer), WTFMove(completionHandler)));
return std::unique_ptr<GameControllerHapticEffect>(new GameControllerHapticEffect(WTFMove(leftPlayer), WTFMove(rightPlayer)));
}

GameControllerHapticEffect::GameControllerHapticEffect(RetainPtr<id>&& strongPlayer, RetainPtr<id>&& weakPlayer, CompletionHandler<void(bool)>&& completionHandler)
: m_strongPlayer(WTFMove(strongPlayer))
, m_weakPlayer(WTFMove(weakPlayer))
, m_completionHandler(WTFMove(completionHandler))
GameControllerHapticEffect::GameControllerHapticEffect(RetainPtr<id>&& leftPlayer, RetainPtr<id>&& rightPlayer)
: m_leftPlayer(WTFMove(leftPlayer))
, m_rightPlayer(WTFMove(rightPlayer))
{
}

Expand All @@ -94,40 +107,44 @@ static double magnitudeToIntensity(double magnitude)
m_completionHandler(false);
}

bool GameControllerHapticEffect::start()
void GameControllerHapticEffect::start(CompletionHandler<void(bool)>&& completionHandler)
{
ASSERT(!m_completionHandler);
m_completionHandler = WTFMove(completionHandler);

NSError *error;
if (m_strongPlayer && ![m_strongPlayer startAtTime:0 error:&error]) {
if (m_leftPlayer && ![m_leftPlayer startAtTime:0 error:&error]) {
RELEASE_LOG_ERROR(Gamepad, "GameControllerHapticEffect::start: Failed to start the strong player");
m_strongPlayer = nullptr;
m_leftPlayer = nullptr;
}
if (m_weakPlayer && ![m_weakPlayer startAtTime:0 error:&error]) {
if (m_rightPlayer && ![m_rightPlayer startAtTime:0 error:&error]) {
RELEASE_LOG_ERROR(Gamepad, "GameControllerHapticEffect::start: Failed to start the weak player");
m_weakPlayer = nullptr;
m_rightPlayer = nullptr;
}
return m_strongPlayer || m_weakPlayer;
if (!m_leftPlayer && !m_rightPlayer)
m_completionHandler(false);
}

void GameControllerHapticEffect::stop()
{
NSError *error;
if (auto player = std::exchange(m_strongPlayer, nullptr))
if (auto player = std::exchange(m_leftPlayer, nullptr))
[player stopAtTime:0.0 error:&error];
if (auto player = std::exchange(m_weakPlayer, nullptr))
if (auto player = std::exchange(m_rightPlayer, nullptr))
[player stopAtTime:0.0 error:&error];
}

void GameControllerHapticEffect::strongEffectFinishedPlaying()
void GameControllerHapticEffect::leftEffectFinishedPlaying()
{
m_strongPlayer = nullptr;
if (!m_weakPlayer && m_completionHandler)
m_leftPlayer = nullptr;
if (!m_rightPlayer && m_completionHandler)
m_completionHandler(true);
}

void GameControllerHapticEffect::weakEffectFinishedPlaying()
void GameControllerHapticEffect::rightEffectFinishedPlaying()
{
m_weakPlayer = nullptr;
if (!m_strongPlayer && m_completionHandler)
m_rightPlayer = nullptr;
if (!m_leftPlayer && m_completionHandler)
m_completionHandler(true);
}

Expand Down
Expand Up @@ -51,19 +51,27 @@ class GameControllerHapticEngines : public CanMakeWeakPtr<GameControllerHapticEn

void stop(CompletionHandler<void()>&&);

CHHapticEngine *strongEngine() { return m_strongEngine.get(); }
CHHapticEngine *weakEngine() { return m_weakEngine.get(); }
CHHapticEngine *leftHandleEngine() { return m_leftHandleEngine.get(); }
CHHapticEngine *rightHandleEngine() { return m_rightHandleEngine.get(); }
CHHapticEngine *leftTriggerEngine() { return m_leftTriggerEngine.get(); }
CHHapticEngine *rightTriggerEngine() { return m_rightTriggerEngine.get(); }

private:
explicit GameControllerHapticEngines(GCController *);

void ensureStarted(CompletionHandler<void(bool)>&&);
void ensureStarted(GamepadHapticEffectType, CompletionHandler<void(bool)>&&);
std::unique_ptr<GameControllerHapticEffect>& currentEffectForType(GamepadHapticEffectType);

RetainPtr<CHHapticEngine> m_strongEngine;
RetainPtr<CHHapticEngine> m_weakEngine;
bool m_failedToStartStrongEngine { false };
bool m_failedToStartWeakEngine { false };
std::unique_ptr<GameControllerHapticEffect> m_currentEffect;
RetainPtr<CHHapticEngine> m_leftHandleEngine;
RetainPtr<CHHapticEngine> m_rightHandleEngine;
RetainPtr<CHHapticEngine> m_leftTriggerEngine;
RetainPtr<CHHapticEngine> m_rightTriggerEngine;
bool m_failedToStartLeftHandleEngine { false };
bool m_failedToStartRightHandleEngine { false };
bool m_failedToStartLeftTriggerEngine { false };
bool m_failedToStartRightTriggerEngine { false };
std::unique_ptr<GameControllerHapticEffect> m_currentDualRumbleEffect;
std::unique_ptr<GameControllerHapticEffect> m_currentTriggerRumbleEffect;
};

} // namespace WebCore
Expand Down

0 comments on commit 2dca512

Please sign in to comment.