// Exercise the production controller with deterministic host output and time. #include #include #include #include #include #include namespace { void Check(bool condition, const char* text) { if (!condition) { std::fprintf(stderr, "ControllerSettingsTests: %s\n", text); std::abort(); } } struct Rumble { Uint16 large, small; Uint32 duration; }; Uint64 now = 1000; std::vector rumble; std::vector haptics; std::vector> effects; bool haptics_handles_rumble = false; } // namespace namespace Fake { Uint64 GetTicks() { return now; } SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) { return id == 1 || id == 2 ? reinterpret_cast(static_cast(id)) : nullptr; } SDL_GamepadType GetGamepadType(SDL_Gamepad*) { return SDL_GAMEPAD_TYPE_PS5; } bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) { rumble.push_back({large, small, duration}); return true; } bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) { Check(size == 32, "unexpected DualSense effect size"); std::array effect {}; std::memcpy(effect.data(), data, effect.size()); effects.push_back(effect); return true; } bool SetGamepadLED(SDL_Gamepad*, Uint8, Uint8, Uint8) { return true; } bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) { return false; } bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) { return true; } void CloseGamepad(SDL_Gamepad*) {} void Delay(Uint32) {} } // namespace Fake #define SDL_GetTicks Fake::GetTicks #define SDL_GetGamepadFromID Fake::GetGamepadFromID #define SDL_GetGamepadType Fake::GetGamepadType #define SDL_RumbleGamepad Fake::RumbleGamepad #define SDL_SendGamepadEffect Fake::SendGamepadEffect #define SDL_SetGamepadLED Fake::SetGamepadLED #define SDL_GamepadHasSensor Fake::GamepadHasSensor #define SDL_SetGamepadSensorEnabled Fake::SetGamepadSensorEnabled #define SDL_CloseGamepad Fake::CloseGamepad #define SDL_Delay Fake::Delay #include "libs/controller.cpp" #undef SDL_GetTicks #undef SDL_GetGamepadFromID #undef SDL_GetGamepadType #undef SDL_RumbleGamepad #undef SDL_SendGamepadEffect #undef SDL_SetGamepadLED #undef SDL_GamepadHasSensor #undef SDL_SetGamepadSensorEnabled #undef SDL_CloseGamepad #undef SDL_Delay namespace Libs::Controller::DualSenseHaptics { bool SetVibration(int, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms) { haptics.push_back({large_motor, small_motor, duration_ms}); return haptics_handles_rumble; } void Shutdown() {} } // namespace Libs::Controller::DualSenseHaptics namespace Libs::LibKernel { uint64_t KYTY_SYSV_ABI KernelGetProcessTime() { return now * 1000; } } // namespace Libs::LibKernel namespace Loader::Timer { double GetTimeMs() { return static_cast(now); } } // namespace Loader::Timer namespace { using namespace Libs::Controller; struct Controller { Controller() { now = 1000; haptics_handles_rumble = false; Initialize(); Connect(1); Check(GetSettingScale(Setting::SpeakerVolume) == Config::GetControllerSpeakerVolume() / 50.0f && GetSettingScale(Setting::VibrationIntensity) == Config::GetControllerVibrationIntensity() / 100.0f && GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f, "controller initialization retained old settings"); rumble.clear(); haptics.clear(); effects.clear(); } ~Controller() { Shutdown(); } }; DualSenseEffects LastEffect() { Check(!effects.empty(), "missing trigger output"); DualSenseEffects effect {}; std::memcpy(&effect, effects.back().data(), sizeof(effect)); return effect; } int ZoneStrength(const uint8_t* effect, int zone) { const unsigned active = effect[1] | (effect[2] << 8u); const uint32_t packed = effect[3] | (effect[4] << 8u) | (effect[5] << 16u) | (static_cast(effect[6]) << 24u); return (active & (1u << zone)) != 0 ? 1 + ((packed >> (3 * zone)) & 7u) : 0; } void SetRumble(uint8_t large, uint8_t small) { const PadVibrationParam param {large, small}; Check(PadSetVibration(1, ¶m) == 0, "vibration request failed"); } void TestSettingCycles() { Controller controller; for (auto setting: {Setting::SpeakerVolume, Setting::VibrationIntensity, Setting::TriggerEffectIntensity}) { Check(GetSettingScale(setting) == 1.0f, "initial setting is not strong"); const std::vector levels = setting == Setting::SpeakerVolume ? std::vector {0.0f, 0.02f, 0.12f, 0.42f, 1.0f} : std::vector {0.0f, 0.33f, 0.66f, 1.0f}; for (int cycle = 0; cycle < 2; ++cycle) { for (float level: levels) { CycleSetting(setting); Check(GetSettingScale(setting) == level, "setting did not cycle or wrap"); } } } CycleSetting(Setting::SpeakerVolume); CycleSetting(Setting::VibrationIntensity); CycleSetting(Setting::TriggerEffectIntensity); } void TestGlobalControllerLevels() { Config::ConfigOptions options; options.controller_speaker_volume = 25; options.controller_vibration_intensity = 25; Config::Load(options); { Controller controller; Check(GetSettingScale(Setting::SpeakerVolume) == 0.5f && GetSettingScale(Setting::VibrationIntensity) == 0.25f && GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f, "global controller levels did not scale their outputs independently"); SetRumble(200, 100); Check(rumble.back().large == 50 * 257 && rumble.back().small == 25 * 257, "global vibration level did not scale rumble"); CycleSetting(Setting::SpeakerVolume); CycleSetting(Setting::VibrationIntensity); Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f && GetSettingScale(Setting::VibrationIntensity) == 0.0f, "cycle hotkeys did not mute globally scaled outputs"); CycleSetting(Setting::SpeakerVolume); CycleSetting(Setting::VibrationIntensity); Check(GetSettingScale(Setting::SpeakerVolume) == 0.01f && GetSettingScale(Setting::VibrationIntensity) == 0.0825f, "cycle hotkeys did not multiply the global controller levels"); Check(rumble.back().large == 17 * 257 && rumble.back().small == 8 * 257, "cycling vibration did not apply the combined intensity to cached rumble"); } options.controller_speaker_volume = 50; Config::Load(options); { Controller controller; Check(GetSettingScale(Setting::SpeakerVolume) == 1.0f, "speaker midpoint did not preserve the previous default level"); } options.controller_speaker_volume = 100; Config::Load(options); { Controller controller; Check(GetSettingScale(Setting::SpeakerVolume) == 2.0f, "speaker maximum did not add 6 dB of gain"); CycleSetting(Setting::SpeakerVolume); Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f, "speaker maximum prevented the hotkey from muting"); } options.controller_speaker_volume = 0; options.controller_vibration_intensity = 0; Config::Load(options); { Controller controller; Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f && GetSettingScale(Setting::VibrationIntensity) == 0.0f, "zero global controller levels did not mute outputs"); SetRumble(200, 100); Check(rumble.back().large == 0 && rumble.back().small == 0, "zero global vibration level did not mute motors"); } Config::Load(Config::ConfigOptions {}); } void TestVibrationLifetime() { Controller controller; SetRumble(255, 1); Check(rumble.size() == 1 && rumble.back().large == 65535 && rumble.back().small == 257 && rumble.back().duration == 65535, "full vibration changed"); now += 100; CycleSetting(Setting::VibrationIntensity); Check(rumble.back().large == 0 && rumble.back().small == 0, "muting did not stop vibration"); now += 100; CycleSetting(Setting::VibrationIntensity); Check(rumble.back().large == 84 * 257 && rumble.back().small == 257 && rumble.back().duration == 65335, "changing intensity lost a small motor or extended the vibration deadline"); Check(effects.empty(), "vibration intensity resent triggers"); now = 1000 + 65535; const auto calls = rumble.size(); CycleSetting(Setting::VibrationIntensity); CycleSetting(Setting::VibrationIntensity); Check(rumble.size() == calls || (rumble.back().large == 0 && rumble.back().small == 0), "expired vibration was resurrected"); SetRumble(0, 0); const auto stopped = rumble.size(); CycleSetting(Setting::VibrationIntensity); CycleSetting(Setting::VibrationIntensity); Check(rumble.size() == stopped || (rumble.back().large == 0 && rumble.back().small == 0), "explicitly stopped vibration was resurrected"); } void TestMaskedTriggersAndValidation() { Controller controller; PadTriggerEffectParam param {}; param.trigger_mask = 3; param.command[0].mode = 1; param.command[0].data[0] = 2; param.command[0].data[1] = 8; param.command[1].mode = 2; param.command[1].data[0] = 2; param.command[1].data[1] = 7; param.command[1].data[2] = 6; Check(PadSetTriggerEffect(1, ¶m) == 0, "initial trigger request failed"); CycleSetting(Setting::TriggerEffectIntensity); Check(LastEffect().left_trigger[0] == 5 && LastEffect().right_trigger[0] == 5, "muting did not disable both triggers"); for (uint32_t mode = 1; mode <= 7; ++mode) { auto invalid = param; invalid.command[1] = {}; invalid.command[1].mode = mode; std::memset(invalid.command[1].data, 255, sizeof(invalid.command[1].data)); const auto calls = effects.size(); Check(PadSetTriggerEffect(1, &invalid) == PAD_ERROR_INVALID_ARG && effects.size() == calls, "muting bypassed trigger validation or sent a partial invalid request"); } param.trigger_mask = 4; Check(PadSetTriggerEffect(1, ¶m) == PAD_ERROR_INVALID_ARG, "invalid trigger mask accepted"); param.trigger_mask = 1; param.command[0].data[1] = 6; Check(PadSetTriggerEffect(1, ¶m) == 0 && LastEffect().enable_bits == 8, "left-only update touched the right trigger"); CycleSetting(Setting::TriggerEffectIntensity); auto effect = LastEffect(); Check(effect.enable_bits == 12 && effect.left_trigger[0] == 0x21 && ZoneStrength(effect.left_trigger, 1) == 0 && ZoneStrength(effect.left_trigger, 2) == 2 && effect.right_trigger[0] == 0x25 && effect.right_trigger[1] == 0x84 && effect.right_trigger[3] == 1, "masked updates lost a cached trigger or failed to scale its strength"); CycleSetting(Setting::TriggerEffectIntensity); CycleSetting(Setting::TriggerEffectIntensity); effect = LastEffect(); Check(ZoneStrength(effect.left_trigger, 2) == 6 && effect.right_trigger[3] == 5, "restoring strong intensity did not recover original trigger strengths"); Check(rumble.empty() && haptics.empty(), "trigger intensity resent vibration"); } void TestIndependentOutputsAndPadSwitch() { Controller controller; haptics_handles_rumble = true; SetRumble(200, 100); now += 100; CycleSetting(Setting::VibrationIntensity); CycleSetting(Setting::VibrationIntensity); Check(rumble.empty() && haptics.back().large == 66 && haptics.back().small == 33 && haptics.back().duration == 65435, "haptics rumble did not receive scaled motors and remaining duration"); PadTriggerEffectParam param {}; param.trigger_mask = 1; param.command[0].mode = 1; param.command[0].data[1] = 8; Check(PadSetTriggerEffect(1, ¶m) == 0, "trigger request failed"); const auto vibration_calls = haptics.size(); const auto trigger_calls = effects.size(); CycleSetting(Setting::SpeakerVolume); Check(haptics.size() == vibration_calls && effects.size() == trigger_calls, "speaker volume resent controller effects"); Connect(2); Disconnect(1); Check(GetActiveControllerId() == 2, "active controller did not change"); haptics.clear(); effects.clear(); CycleSetting(Setting::VibrationIntensity); CycleSetting(Setting::TriggerEffectIntensity); CycleSetting(Setting::TriggerEffectIntensity); Check(effects.empty(), "previous controller's trigger effects leaked to new pad"); for (const auto& request: haptics) { Check(request.large == 0 && request.small == 0, "previous controller's vibration leaked to new pad"); } SetRumble(200, 100); Check(PadSetTriggerEffect(1, ¶m) == 0, "replacement pad trigger request failed"); EmergencyShutdown(); Check(GetActiveControllerId() == -1, "released controller remained active"); haptics.clear(); effects.clear(); CycleSetting(Setting::VibrationIntensity); CycleSetting(Setting::TriggerEffectIntensity); Check(effects.empty(), "released controller received a trigger request"); for (const auto& request: haptics) { Check(request.large == 0 && request.small == 0, "released controller retained a cached vibration"); } } } // namespace int main() { Config::Initialize(); TestSettingCycles(); TestGlobalControllerLevels(); // Each following test initializes another controller and requires strong defaults. TestVibrationLifetime(); TestMaskedTriggersAndValidation(); TestIndependentOutputsAndPadSwitch(); Config::Shutdown(); return 0; }