From d9ebb975b28b6673e7231e7ab5cdf1350d5d7a93 Mon Sep 17 00:00:00 2001
From: Connor McLaughlin <stenzek@gmail.com>
Date: Tue, 14 Apr 2020 16:34:39 +1000
Subject: [PATCH] CommonHostInterface: Reimplement controller rumble support

Even better than before, supports separate motor control.
---
 src/core/analog_controller.cpp                |   5 +
 src/core/analog_controller.h                  |   1 +
 src/core/controller.cpp                       |  16 +++
 src/core/controller.h                         |   3 +
 src/core/digital_controller.cpp               |   5 +
 src/core/digital_controller.h                 |   1 +
 src/core/gpu_hw_opengl.cpp                    |   9 +-
 src/core/gpu_hw_shadergen.cpp                 |   2 +-
 src/duckstation-qt/qthostinterface.cpp        |   1 +
 src/duckstation-sdl/sdl_host_interface.cpp    |   3 +-
 src/frontend-common/common_host_interface.cpp | 104 +++++++++++++++++-
 src/frontend-common/common_host_interface.h   |  22 ++++
 src/frontend-common/controller_interface.cpp  |   1 -
 src/frontend-common/controller_interface.h    |   7 +-
 .../sdl_controller_interface.cpp              |  92 ++++++++++++----
 .../sdl_controller_interface.h                |   8 +-
 16 files changed, 242 insertions(+), 38 deletions(-)

diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp
index cc99e15b8..08770a851 100644
--- a/src/core/analog_controller.cpp
+++ b/src/core/analog_controller.cpp
@@ -436,3 +436,8 @@ Controller::ButtonList AnalogController::StaticGetButtonNames()
           B(Square), B(L1),   B(L2),    B(R1),     B(R2),    B(L3),       B(R3)};
 #undef B
 }
+
+u32 AnalogController::StaticGetVibrationMotorCount()
+{
+  return NUM_MOTORS;
+}
diff --git a/src/core/analog_controller.h b/src/core/analog_controller.h
index 1449a8779..74ba6fbbe 100644
--- a/src/core/analog_controller.h
+++ b/src/core/analog_controller.h
@@ -48,6 +48,7 @@ public:
   static std::optional<s32> StaticGetButtonCodeByName(std::string_view button_name);
   static AxisList StaticGetAxisNames();
   static ButtonList StaticGetButtonNames();
+  static u32 StaticGetVibrationMotorCount();
 
   ControllerType GetType() const override;
   std::optional<s32> GetAxisCodeByName(std::string_view axis_name) const override;
diff --git a/src/core/controller.cpp b/src/core/controller.cpp
index 9ce77e9d9..1ec67eab5 100644
--- a/src/core/controller.cpp
+++ b/src/core/controller.cpp
@@ -94,6 +94,22 @@ Controller::ButtonList Controller::GetButtonNames(ControllerType type)
   }
 }
 
+u32 Controller::GetVibrationMotorCount(ControllerType type)
+{
+  switch (type)
+  {
+    case ControllerType::DigitalController:
+      return DigitalController::StaticGetVibrationMotorCount();
+
+    case ControllerType::AnalogController:
+      return AnalogController::StaticGetVibrationMotorCount();
+
+    case ControllerType::None:
+    default:
+      return 0;
+  }
+}
+
 std::optional<s32> Controller::GetAxisCodeByName(ControllerType type, std::string_view axis_name)
 {
   switch (type)
diff --git a/src/core/controller.h b/src/core/controller.h
index 573e0a5fa..5f05deae0 100644
--- a/src/core/controller.h
+++ b/src/core/controller.h
@@ -61,4 +61,7 @@ public:
 
   /// Returns a list of buttons for the specified controller type.
   static ButtonList GetButtonNames(ControllerType type);
+
+  /// Returns the number of vibration motors.
+  static u32 GetVibrationMotorCount(ControllerType type);
 };
diff --git a/src/core/digital_controller.cpp b/src/core/digital_controller.cpp
index 821b5c0d0..5add08426 100644
--- a/src/core/digital_controller.cpp
+++ b/src/core/digital_controller.cpp
@@ -147,3 +147,8 @@ Controller::ButtonList DigitalController::StaticGetButtonNames()
           B(Cross), B(Circle), B(Square), B(L1),    B(L2),     B(R1),    B(R2)};
 #undef B
 }
+
+u32 DigitalController::StaticGetVibrationMotorCount()
+{
+  return 0;
+}
diff --git a/src/core/digital_controller.h b/src/core/digital_controller.h
index 58dbb465c..e970c64f4 100644
--- a/src/core/digital_controller.h
+++ b/src/core/digital_controller.h
@@ -36,6 +36,7 @@ public:
   static std::optional<s32> StaticGetButtonCodeByName(std::string_view button_name);
   static AxisList StaticGetAxisNames();
   static ButtonList StaticGetButtonNames();
+  static u32 StaticGetVibrationMotorCount();
 
   ControllerType GetType() const override;
   std::optional<s32> GetAxisCodeByName(std::string_view axis_name) const override;
diff --git a/src/core/gpu_hw_opengl.cpp b/src/core/gpu_hw_opengl.cpp
index a70bc192a..8b29fabe1 100644
--- a/src/core/gpu_hw_opengl.cpp
+++ b/src/core/gpu_hw_opengl.cpp
@@ -163,6 +163,7 @@ void GPU_HW_OpenGL::SetCapabilities(HostDisplay* host_display)
     Log_WarningPrintf("GL_EXT_copy_image missing, this may affect performance.");
 
   m_supports_texture_buffer = (GLAD_GL_VERSION_3_1 || GLAD_GL_ES_VERSION_3_2);
+  m_supports_texture_buffer = false;
   if (m_supports_texture_buffer)
   {
     glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE, reinterpret_cast<GLint*>(&m_max_texture_buffer_size));
@@ -766,7 +767,7 @@ void GPU_HW_OpenGL::CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 wid
   src_y = m_vram_texture.GetHeight() - src_y - height;
   dst_y = m_vram_texture.GetHeight() - dst_y - height;
 
-  if (GLAD_GL_VERSION_4_3)
+  /*if (GLAD_GL_VERSION_4_3)
   {
     glCopyImageSubData(m_vram_texture.GetGLId(), GL_TEXTURE_2D, 0, src_x, src_y, 0, m_vram_texture.GetGLId(),
                        GL_TEXTURE_2D, 0, dst_x, dst_y, 0, width, height, 1);
@@ -776,7 +777,7 @@ void GPU_HW_OpenGL::CopyVRAM(u32 src_x, u32 src_y, u32 dst_x, u32 dst_y, u32 wid
     glCopyImageSubDataEXT(m_vram_texture.GetGLId(), GL_TEXTURE_2D, 0, src_x, src_y, 0, m_vram_texture.GetGLId(),
                           GL_TEXTURE_2D, 0, dst_x, dst_y, 0, width, height, 1);
   }
-  else
+  else*/
   {
     glDisable(GL_SCISSOR_TEST);
     m_vram_texture.BindFramebuffer(GL_READ_FRAMEBUFFER);
@@ -794,7 +795,7 @@ void GPU_HW_OpenGL::UpdateVRAMReadTexture()
   const u32 x = scaled_rect.left;
   const u32 y = m_vram_texture.GetHeight() - scaled_rect.top - height;
 
-  if (GLAD_GL_VERSION_4_3)
+  /*if (GLAD_GL_VERSION_4_3)
   {
     glCopyImageSubData(m_vram_texture.GetGLId(), GL_TEXTURE_2D, 0, x, y, 0, m_vram_read_texture.GetGLId(),
                        GL_TEXTURE_2D, 0, x, y, 0, width, height, 1);
@@ -804,7 +805,7 @@ void GPU_HW_OpenGL::UpdateVRAMReadTexture()
     glCopyImageSubDataEXT(m_vram_texture.GetGLId(), GL_TEXTURE_2D, 0, x, y, 0, m_vram_read_texture.GetGLId(),
                           GL_TEXTURE_2D, 0, x, y, 0, width, height, 1);
   }
-  else
+  else*/
   {
     m_vram_read_texture.BindFramebuffer(GL_DRAW_FRAMEBUFFER);
     m_vram_texture.BindFramebuffer(GL_READ_FRAMEBUFFER);
diff --git a/src/core/gpu_hw_shadergen.cpp b/src/core/gpu_hw_shadergen.cpp
index 0b65932ba..8bbf023fa 100644
--- a/src/core/gpu_hw_shadergen.cpp
+++ b/src/core/gpu_hw_shadergen.cpp
@@ -547,7 +547,7 @@ float4 SampleFromVRAM(int4 texpage, int2 icoord)
       texcol.rgb /= float3(ialpha, ialpha, ialpha);
       semitransparent = (texcol.a != 0.0);
     #else
-      float4 texcol = SampleFromVRAM(v_texpage, int2(v_tex0));
+      float4 texcol = SampleFromVRAM(v_texpage, int2(floor(v_tex0)));
       if (VECTOR_EQ(texcol, TRANSPARENT_PIXEL_COLOR))
         discard;
 
diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp
index ae4662ed5..8281388a7 100644
--- a/src/duckstation-qt/qthostinterface.cpp
+++ b/src/duckstation-qt/qthostinterface.cpp
@@ -792,6 +792,7 @@ void QtHostInterface::threadEntryPoint()
     }
 
     m_system->RunFrame();
+    UpdateControllerRumble();
 
     renderDisplay();
 
diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp
index 52a08c69b..1175a8433 100644
--- a/src/duckstation-sdl/sdl_host_interface.cpp
+++ b/src/duckstation-sdl/sdl_host_interface.cpp
@@ -1301,10 +1301,11 @@ void SDLHostInterface::Run()
     if (m_system && !m_paused)
     {
       m_system->RunFrame();
+      UpdateControllerRumble();
       if (m_frame_step_request)
       {
         m_frame_step_request = false;
-        m_paused = true;
+        PauseSystem(true);
       }
     }
 
diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp
index 7a06ed096..9b6716860 100644
--- a/src/frontend-common/common_host_interface.cpp
+++ b/src/frontend-common/common_host_interface.cpp
@@ -335,8 +335,20 @@ void CommonHostInterface::OnSystemPaused(bool paused)
 {
   HostInterface::OnSystemPaused(paused);
 
-  if (paused && IsFullscreen())
-    SetFullscreen(false);
+  if (paused)
+  {
+    if (IsFullscreen())
+      SetFullscreen(false);
+
+    StopControllerRumble();
+  }
+}
+
+void CommonHostInterface::OnSystemDestroyed()
+{
+  HostInterface::OnSystemDestroyed();
+
+  StopControllerRumble();
 }
 
 void CommonHostInterface::OnControllerTypeChanged(u32 slot)
@@ -405,6 +417,55 @@ void CommonHostInterface::UpdateInputMap(SettingsInterface& si)
   UpdateHotkeyInputMap(si);
 }
 
+void CommonHostInterface::AddControllerRumble(u32 controller_index, u32 num_motors, ControllerRumbleCallback callback)
+{
+  ControllerRumbleState rumble;
+  rumble.controller_index = 0;
+  rumble.num_motors = std::min<u32>(num_motors, ControllerRumbleState::MAX_MOTORS);
+  rumble.last_strength.fill(0.0f);
+  rumble.update_callback = std::move(callback);
+  m_controller_vibration_motors.push_back(std::move(rumble));
+}
+
+void CommonHostInterface::UpdateControllerRumble()
+{
+  DebugAssert(m_system);
+
+  for (ControllerRumbleState& rumble : m_controller_vibration_motors)
+  {
+    Controller* controller = m_system->GetController(rumble.controller_index);
+    if (!controller)
+      continue;
+
+    bool changed = false;
+    for (u32 i = 0; i < rumble.num_motors; i++)
+    {
+      const float strength = controller->GetVibrationMotorStrength(i);
+      changed |= (strength != rumble.last_strength[i]);
+      rumble.last_strength[i] = strength;
+    }
+
+    if (changed)
+      rumble.update_callback(rumble.last_strength.data(), rumble.num_motors);
+  }
+}
+
+void CommonHostInterface::StopControllerRumble()
+{
+  for (ControllerRumbleState& rumble : m_controller_vibration_motors)
+  {
+    bool changed = true;
+    for (u32 i = 0; i < rumble.num_motors; i++)
+    {
+      changed |= (rumble.last_strength[i] != 0.0f);
+      rumble.last_strength[i] = 0.0f;
+    }
+
+    if (changed)
+      rumble.update_callback(rumble.last_strength.data(), rumble.num_motors);
+  }
+}
+
 static bool SplitBinding(const std::string& binding, std::string_view* device, std::string_view* sub_binding)
 {
   const std::string::size_type slash_pos = binding.find('/');
@@ -421,6 +482,9 @@ static bool SplitBinding(const std::string& binding, std::string_view* device, s
 
 void CommonHostInterface::UpdateControllerInputMap(SettingsInterface& si)
 {
+  StopControllerRumble();
+  m_controller_vibration_motors.clear();
+
   for (u32 controller_index = 0; controller_index < 2; controller_index++)
   {
     const ControllerType ctype = m_settings.controller_types[controller_index];
@@ -477,6 +541,14 @@ void CommonHostInterface::UpdateControllerInputMap(SettingsInterface& si)
         });
       }
     }
+
+    const u32 num_motors = Controller::GetVibrationMotorCount(ctype);
+    if (num_motors > 0)
+    {
+      const std::vector<std::string> bindings = si.GetStringList(category, TinyString::FromFormat("Rumble"));
+      for (const std::string& binding : bindings)
+        AddRumbleToInputMap(binding, controller_index, num_motors);
+    }
   }
 }
 
@@ -600,6 +672,34 @@ bool CommonHostInterface::AddAxisToInputMap(const std::string& binding, const st
   return false;
 }
 
+bool CommonHostInterface::AddRumbleToInputMap(const std::string& binding, u32 controller_index, u32 num_motors)
+{
+  if (StringUtil::StartsWith(binding, "Controller"))
+  {
+    if (!m_controller_interface)
+    {
+      Log_ErrorPrintf("No controller interface set, cannot bind '%s'", binding.c_str());
+      return false;
+    }
+
+    const std::optional<int> host_controller_index = StringUtil::FromChars<int>(binding.substr(10));
+    if (!host_controller_index || *host_controller_index < 0)
+    {
+      Log_WarningPrintf("Invalid controller index in rumble binding '%s'", binding.c_str());
+      return false;
+    }
+
+    AddControllerRumble(controller_index, num_motors,
+                        std::bind(&ControllerInterface::SetControllerRumbleStrength, m_controller_interface.get(),
+                                  host_controller_index.value(), std::placeholders::_1, std::placeholders::_2));
+
+    return true;
+  }
+
+  Log_WarningPrintf("Unknown input device in rumble binding '%s'", binding.c_str());
+  return false;
+}
+
 void CommonHostInterface::RegisterGeneralHotkeys()
 {
   RegisterHotkey(StaticString("General"), StaticString("FastForward"), StaticString("Toggle Fast Forward"),
diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h
index 4b4ea51bc..fd931c1db 100644
--- a/src/frontend-common/common_host_interface.h
+++ b/src/frontend-common/common_host_interface.h
@@ -22,6 +22,7 @@ public:
 
   using InputButtonHandler = std::function<void(bool)>;
   using InputAxisHandler = std::function<void(float)>;
+  using ControllerRumbleCallback = std::function<void(const float*, u32)>;
 
   struct HotkeyInfo
   {
@@ -69,6 +70,7 @@ protected:
 
   virtual void OnSystemCreated() override;
   virtual void OnSystemPaused(bool paused) override;
+  virtual void OnSystemDestroyed() override;
   virtual void OnControllerTypeChanged(u32 slot) override;
 
   virtual void SetDefaultSettings(SettingsInterface& si) override;
@@ -79,6 +81,7 @@ protected:
                                    const std::string_view& button, InputButtonHandler handler);
   virtual bool AddAxisToInputMap(const std::string& binding, const std::string_view& device,
                                  const std::string_view& axis, InputAxisHandler handler);
+  virtual bool AddRumbleToInputMap(const std::string& binding, u32 controller_index, u32 num_motors);
 
   /// Reloads the input map from config. Callable from controller interface.
   virtual void UpdateInputMap() = 0;
@@ -87,6 +90,10 @@ protected:
   bool HandleHostKeyEvent(HostKeyCode code, bool pressed);
   void UpdateInputMap(SettingsInterface& si);
 
+  void AddControllerRumble(u32 controller_index, u32 num_motors, ControllerRumbleCallback callback);
+  void UpdateControllerRumble();
+  void StopControllerRumble();
+
   std::unique_ptr<ControllerInterface> m_controller_interface;
 
 private:
@@ -101,6 +108,21 @@ private:
   // input key maps
   std::map<HostKeyCode, InputButtonHandler> m_keyboard_input_handlers;
 
+  // controller vibration motors/rumble
+  struct ControllerRumbleState
+  {
+    enum : u32
+    {
+      MAX_MOTORS = 2
+    };
+
+    u32 controller_index;
+    u32 num_motors;
+    std::array<float, MAX_MOTORS> last_strength;
+    ControllerRumbleCallback update_callback;
+  };
+  std::vector<ControllerRumbleState> m_controller_vibration_motors;
+
   // running in batch mode? i.e. exit after stopping emulation
   bool m_batch_mode = false;
 };
diff --git a/src/frontend-common/controller_interface.cpp b/src/frontend-common/controller_interface.cpp
index 87da03282..43f4a170f 100644
--- a/src/frontend-common/controller_interface.cpp
+++ b/src/frontend-common/controller_interface.cpp
@@ -90,4 +90,3 @@ bool ControllerInterface::BindControllerAxisToButton(int controller_index, int a
   return false;
 }
 
-void ControllerInterface::UpdateControllerRumble() {}
diff --git a/src/frontend-common/controller_interface.h b/src/frontend-common/controller_interface.h
index e95863abf..ed35e6108 100644
--- a/src/frontend-common/controller_interface.h
+++ b/src/frontend-common/controller_interface.h
@@ -36,9 +36,12 @@ public:
   virtual bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) = 0;
   virtual bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
                                           ButtonCallback callback) = 0;
-  
+
   virtual void PollEvents() = 0;
-  virtual void UpdateControllerRumble() = 0;
+
+  // Changing rumble strength.
+  virtual u32 GetControllerRumbleMotorCount(int controller_index) = 0;
+  virtual void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) = 0;
 
   // Input monitoring for external access.
   struct Hook
diff --git a/src/frontend-common/sdl_controller_interface.cpp b/src/frontend-common/sdl_controller_interface.cpp
index 05e5701c9..53af1b297 100644
--- a/src/frontend-common/sdl_controller_interface.cpp
+++ b/src/frontend-common/sdl_controller_interface.cpp
@@ -161,12 +161,35 @@ bool SDLControllerInterface::OpenGameController(int index)
   cd.controller = gcontroller;
   cd.player_id = player_id;
   cd.joystick_id = joystick_id;
+  cd.haptic_left_right_effect = -1;
 
   SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
-  if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) == 0)
-    cd.haptic = haptic;
-  else if (haptic)
-    SDL_HapticClose(haptic);
+  if (haptic)
+  {
+    SDL_HapticEffect ef = {};
+    ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
+    ef.leftright.length = 1000;
+
+    int ef_id = SDL_HapticNewEffect(haptic, &ef);
+    if (ef_id >= 0)
+    {
+      cd.haptic = haptic;
+      cd.haptic_left_right_effect = ef_id;
+    }
+    else
+    {
+      Log_ErrorPrintf("Failed to create haptic left/right effect: %s", SDL_GetError());
+      if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
+      {
+        cd.haptic = haptic;
+      }
+      else
+      {
+        Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
+        SDL_HapticClose(haptic);
+      }
+    }
+  }
 
   if (cd.haptic)
     Log_InfoPrintf("Rumble is supported on '%s'", SDL_GameControllerName(gcontroller));
@@ -314,31 +337,52 @@ bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_Event* ev)
   return true;
 }
 
-void SDLControllerInterface::UpdateControllerRumble()
+u32 SDLControllerInterface::GetControllerRumbleMotorCount(int controller_index)
 {
-  for (auto& cd : m_controllers)
+  auto it = GetControllerDataForPlayerId(controller_index);
+  if (it == m_controllers.end())
+    return 0;
+
+  return (it->haptic_left_right_effect >= 0) ? 2 : (it->haptic ? 1 : 0);
+}
+
+void SDLControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors)
+{
+  auto it = GetControllerDataForPlayerId(controller_index);
+  if (it == m_controllers.end())
+    return;
+
+  // we'll update before this duration is elapsed
+  static constexpr float MIN_STRENGTH = 0.01f;
+  static constexpr u32 DURATION = 100000;
+
+  SDL_Haptic* haptic = static_cast<SDL_Haptic*>(it->haptic);
+  if (it->haptic_left_right_effect >= 0 && num_motors > 1)
   {
-    // TODO: FIXME proper binding
-    if (!cd.haptic || cd.player_id < 0 || cd.player_id >= 2)
-      continue;
-
-    float new_strength = 0.0f;
-    Controller* controller = GetController(cd.player_id);
-    if (controller)
+    if (strengths[0] >= MIN_STRENGTH || strengths[1] >= MIN_STRENGTH)
     {
-      const u32 motor_count = controller->GetVibrationMotorCount();
-      for (u32 i = 0; i < motor_count; i++)
-        new_strength = std::max(new_strength, controller->GetVibrationMotorStrength(i));
+      SDL_HapticEffect ef;
+      ef.type = SDL_HAPTIC_LEFTRIGHT;
+      ef.leftright.large_magnitude = static_cast<u32>(strengths[0] * 65535.0f);
+      ef.leftright.small_magnitude = static_cast<u32>(strengths[1] * 65535.0f);
+      ef.leftright.length = DURATION;
+      SDL_HapticUpdateEffect(haptic, it->haptic_left_right_effect, &ef);
+      SDL_HapticRunEffect(haptic, it->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
     }
-
-    if (cd.last_rumble_strength == new_strength)
-      continue;
-
-    if (new_strength > 0.01f)
-      SDL_HapticRumblePlay(static_cast<SDL_Haptic*>(cd.haptic), new_strength, 100000);
     else
-      SDL_HapticRumbleStop(static_cast<SDL_Haptic*>(cd.haptic));
+    {
+      SDL_HapticStopEffect(haptic, it->haptic_left_right_effect);
+    }
+  }
+  else
+  {
+    float max_strength = 0.0f;
+    for (u32 i = 0; i < num_motors; i++)
+      max_strength = std::max(max_strength, strengths[i]);
 
-    cd.last_rumble_strength = new_strength;
+    if (max_strength >= MIN_STRENGTH)
+      SDL_HapticRumblePlay(haptic, max_strength, DURATION);
+    else
+      SDL_HapticRumbleStop(haptic);
   }
 }
diff --git a/src/frontend-common/sdl_controller_interface.h b/src/frontend-common/sdl_controller_interface.h
index 08a0493b8..6293b7268 100644
--- a/src/frontend-common/sdl_controller_interface.h
+++ b/src/frontend-common/sdl_controller_interface.h
@@ -25,20 +25,22 @@ public:
   bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
   bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction, ButtonCallback callback) override;
 
+  // Changing rumble strength.
+  u32 GetControllerRumbleMotorCount(int controller_index) override;
+  void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override;
+
   void PollEvents() override;
 
   bool ProcessSDLEvent(const SDL_Event* event);
 
-  void UpdateControllerRumble() override;
-
 private:
   struct ControllerData
   {
     void* controller;
     void* haptic;
+    int haptic_left_right_effect;
     int joystick_id;
     int player_id;
-    float last_rumble_strength;
 
     std::array<AxisCallback, MAX_NUM_AXISES> axis_mapping;
     std::array<ButtonCallback, MAX_NUM_BUTTONS> button_mapping;