// Exercise the production module with real SDL streams and fake USB endpoints. #include #include #include #include #include #include #include #include namespace { void Check(bool condition, const char* text) { if (!condition) { std::fprintf(stderr, "PadHapticsTests: %s\n", text); std::abort(); } } struct Device { SDL_AudioDeviceID id; const char* name; int channels; }; struct Rumble { Uint16 large, small; Uint32 duration; }; std::vector devices; std::vector streams; Rumble rumble {}; Uint64 now = 1000; int audio_refs = 0, opens = 0, rumble_calls = 0; bool fail_open = false, fail_resume = false; int actual_channels = 4; SDL_AudioDeviceID opened_device = 0; SDL_AudioSpec opened_spec {}; } // namespace namespace Fake { Uint64 GetTicks() { return now; } bool InitSubSystem(SDL_InitFlags flags) { const bool result = SDL_InitSubSystem(flags); if (result && (flags & SDL_INIT_AUDIO)) { audio_refs++; } return result; } void QuitSubSystem(SDL_InitFlags flags) { if (flags & SDL_INIT_AUDIO) { audio_refs--; } SDL_QuitSubSystem(flags); } SDL_AudioDeviceID* GetAudioPlaybackDevices(int* count) { *count = static_cast(devices.size()); auto* ids = static_cast( SDL_malloc((devices.size() + 1) * sizeof(SDL_AudioDeviceID))); for (size_t i = 0; i < devices.size(); i++) { ids[i] = devices[i].id; } ids[devices.size()] = 0; return ids; } const char* GetAudioDeviceName(SDL_AudioDeviceID id) { for (const auto& device: devices) { if (device.id == id) { return device.name; } } return nullptr; } bool GetAudioDeviceFormat(SDL_AudioDeviceID id, SDL_AudioSpec* spec, int*) { if (id == 999) { *spec = {SDL_AUDIO_F32, actual_channels, 48000}; return true; } for (const auto& device: devices) { if (device.id == id) { *spec = {SDL_AUDIO_F32, device.channels, 48000}; return true; } } return false; } SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID id, const SDL_AudioSpec* spec, SDL_AudioStreamCallback callback, void* userdata) { opens++; opened_device = id; opened_spec = *spec; if (fail_open) { return nullptr; } auto* stream = SDL_CreateAudioStream(spec, spec); Check(stream != nullptr && SDL_SetAudioStreamGetCallback(stream, callback, userdata), "real SDL stream/callback creation failed"); streams.push_back(stream); return stream; } bool ResumeAudioStreamDevice(SDL_AudioStream*) { return !fail_resume; } void DestroyAudioStream(SDL_AudioStream* stream) { if (stream == nullptr) { return; } const auto it = std::find(streams.begin(), streams.end(), stream); Check(it != streams.end(), "stream destroyed twice"); streams.erase(it); SDL_DestroyAudioStream(stream); } SDL_AudioDeviceID GetAudioStreamDevice(SDL_AudioStream*) { return 999; } SDL_GamepadType GetGamepadTypeForID(SDL_JoystickID id) { return id == 1 ? SDL_GAMEPAD_TYPE_PS5 : SDL_GAMEPAD_TYPE_UNKNOWN; } SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) { return id == 1 ? reinterpret_cast(static_cast(id)) : nullptr; } bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) { rumble = {large, small, duration}; rumble_calls++; return true; } } // namespace Fake #define SDL_GetTicks Fake::GetTicks #define SDL_InitSubSystem Fake::InitSubSystem #define SDL_QuitSubSystem Fake::QuitSubSystem #define SDL_GetAudioPlaybackDevices Fake::GetAudioPlaybackDevices #define SDL_GetAudioDeviceName Fake::GetAudioDeviceName #define SDL_GetAudioDeviceFormat Fake::GetAudioDeviceFormat #define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream #define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice #define SDL_DestroyAudioStream Fake::DestroyAudioStream #define SDL_GetAudioStreamDevice Fake::GetAudioStreamDevice #define SDL_GetGamepadTypeForID Fake::GetGamepadTypeForID #define SDL_GetGamepadFromID Fake::GetGamepadFromID #define SDL_RumbleGamepad Fake::RumbleGamepad #include "libs/dualSenseHaptics.cpp" #undef SDL_GetTicks #undef SDL_InitSubSystem #undef SDL_QuitSubSystem #undef SDL_GetAudioPlaybackDevices #undef SDL_GetAudioDeviceName #undef SDL_GetAudioDeviceFormat #undef SDL_OpenAudioDeviceStream #undef SDL_ResumeAudioStreamDevice #undef SDL_DestroyAudioStream #undef SDL_GetAudioStreamDevice #undef SDL_GetGamepadTypeForID #undef SDL_GetGamepadFromID #undef SDL_RumbleGamepad namespace { namespace Haptics = Libs::Controller::DualSenseHaptics; using Port = std::unique_ptr; constexpr std::array unity {32768, 32768}; constexpr std::array pcm {0.5f, 0.5f, 0.5f, 0.5f}; struct Fixture { Fixture() { devices = {{10, "Speakers (DualSense Wireless Controller)", 4}}; rumble = {}; now = 1000; opens = rumble_calls = 0; fail_open = fail_resume = false; actual_channels = 4; } ~Fixture() { Haptics::Shutdown(); Check(streams.empty() && audio_refs == 0, "stream or audio subsystem reference leaked"); } }; Port Open() { auto* stream = Haptics::Open(48000); Check(stream != nullptr, "port open failed"); return Port(stream, Haptics::Close); } void Queue(const Port& port, const void* data, uint32_t frames = 2, uint32_t channels = 2, bool is_float = true, const int* volume = unity.data()) { Haptics::Queue(port.get(), 1, data, frames, channels, is_float, volume); } void Pull() { std::array buffer {}; Check(!streams.empty() && SDL_GetAudioStreamData(streams.back(), buffer.data(), sizeof(buffer)) >= 0, "audio device pull failed"); } void ExpectPcm(std::array expected) { std::array actual {}; Check(!streams.empty() && SDL_GetAudioStreamData(streams.back(), actual.data(), sizeof(actual)) == sizeof(actual), "wrong output byte count"); for (size_t i = 0; i < actual.size(); i++) { Check(std::abs(actual[i] - expected[i]) < 1e-6f, "haptics mapping or volume is wrong"); } } void TestFormatsAndVolume() { Fixture f; auto port = Open(); const std::array volume {32768, 16384}; const std::array stereo {0.5f, -0.25f, 1.0f, 0.0f}; Queue(port, stereo.data(), 2, 2, true, volume.data()); Check(opened_device == 10 && opened_spec.channels == 4 && opened_spec.format == SDL_AUDIO_F32, "did not select quad DualSense PCM"); ExpectPcm({0, 0, 0.5f, -0.125f, 0, 0, 1.0f, 0}); const std::array mono {16384, -32768}; Queue(port, mono.data(), 2, 1, false, volume.data() + 1); ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, -0.5f, -0.5f}); std::array multichannel {}; multichannel.fill(0.9f); multichannel[0] = 0.1f; multichannel[1] = 0.2f; multichannel[12] = 0.3f; multichannel[13] = 0.4f; Queue(port, multichannel.data(), 2, 12); ExpectPcm({0, 0, 0.1f, 0.2f, 0, 0, 0.3f, 0.4f}); } void TestDiscoveryAndHotplug() { Fixture f; devices = {{10, "DualSense Wireless Controller", 2}, {20, "Other speakers", 4}}; auto port = Open(); Queue(port, pcm.data()); Check(streams.empty() && opens == 0, "stereo controller or unrelated speakers selected"); devices[0].channels = 4; now += 2001; Queue(port, pcm.data()); Check(streams.size() == 1 && opened_device == 10, "late USB connection was not discovered"); devices.clear(); now += 2001; Queue(port, pcm.data()); Check(streams.empty(), "disconnected endpoint retained its stream"); } void TestFailuresAndBoundedQueue() { Fixture f; auto port = Open(); std::array block {}; block.fill(0.5f); fail_open = true; Queue(port, block.data(), 64); Check(streams.empty() && audio_refs == 1, "failed open leaked a stream/reference"); fail_open = false; fail_resume = true; now += 2001; Queue(port, block.data(), 64); Check(streams.empty() && audio_refs == 1, "failed resume leaked a stream/reference"); fail_resume = false; actual_channels = 2; now += 2001; Queue(port, block.data(), 64); Check(streams.empty(), "post-open stereo downgrade accepted"); actual_channels = 4; now += 2001; for (int i = 0; i < 70; i++) { Queue(port, block.data(), 64); } constexpr int block_bytes = 64 * 4 * sizeof(float); constexpr int max_queue = 48000 * 4 * sizeof(float) * 80 / 1000; Check(streams.size() == 1 && SDL_GetAudioStreamQueued(streams.back()) <= max_queue + block_bytes, "queue grows indefinitely when its clock stalls"); } void TestRumbleLeaseAndDuration() { Fixture f; auto port = Open(); Check(Haptics::SetVibration(1, 100, 50), "set rumble failed"); Check(rumble.large == 100 * 0x101 && rumble.duration == 65535, "ordinary rumble changed"); Queue(port, pcm.data()); Check(rumble.large == 0 && rumble.small == 0, "rumble was not stopped before haptics"); now = 1100; const std::array silence {}; Queue(port, silence.data()); now = 1249; Pull(); Check(rumble.large == 0, "rumble resumed before lease expiration"); now = 1250; Pull(); Check(rumble.large == 100 * 0x101 && rumble.small == 50 * 0x101 && rumble.duration == 65285, "idle haptics did not restore remaining rumble duration"); now = 66500; Queue(port, pcm.data()); now = 66751; Pull(); Check(rumble.large == 0 && rumble.duration == 0, "expired rumble restarted after haptics"); Haptics::Shutdown(); const int calls = rumble_calls; Pull(); Check(rumble_calls == calls, "audio pull touched rumble after shutdown"); } void TestCloseRestoresRumble() { Fixture f; auto first = Open(), second = Open(); Haptics::SetVibration(1, 100, 50); Queue(first, pcm.data()); Queue(second, pcm.data()); Check(rumble.large == 0, "haptics did not suppress rumble"); first.reset(); Check(rumble.large == 0, "closing one of two streams restored rumble early"); second.reset(); Check(rumble.large == 100 * 0x101 && rumble.small == 50 * 0x101, "closing the final stream left rumble suppressed"); } } // namespace int main() { SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); TestFormatsAndVolume(); TestDiscoveryAndHotplug(); TestFailuresAndBoundedQueue(); TestRumbleLeaseAndDuration(); TestCloseRestoresRumble(); std::printf("PadHapticsTests: all cases passed\n"); }