diff --git a/CMakeLists.txt b/CMakeLists.txt index 9952bae3..dcc17072 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -477,6 +477,12 @@ add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL target_link_libraries(ngs2_sampler_tests common fmt::fmt LibAtrac9) target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers}) +add_executable(pad_haptics_tests EXCLUDE_FROM_ALL + "${KYTY_TESTS_DIR}/PadHapticsTests.cpp" +) +target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static) +target_include_directories(pad_haptics_tests PRIVATE ${inc_headers}) + add_executable(save_data_memory_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp" ) @@ -569,6 +575,7 @@ if(BUILD_TESTING) add_test(NAME sync_on_address COMMAND $) add_test(NAME audio_out2_port COMMAND $) add_test(NAME ngs2_sampler COMMAND $) + add_test(NAME pad_haptics COMMAND $) add_test(NAME save_data_memory COMMAND $) add_test(NAME shader_recompiler_compute COMMAND $) add_test(NAME shader_recompiler_alignbyte @@ -636,6 +643,7 @@ if(BUILD_TESTING) virtual_memory_allocation_tests audio_out2_port_tests ngs2_sampler_tests + pad_haptics_tests save_data_memory_tests ) endif() diff --git a/src/libs/audio.cpp b/src/libs/audio.cpp index 428e7992..6ba5672b 100644 --- a/src/libs/audio.cpp +++ b/src/libs/audio.cpp @@ -7,6 +7,8 @@ #include "common/threads.h" #include "kernel/pthread.h" #include "libs/audio_internal.h" +#include "libs/controller.h" +#include "libs/dualSenseHaptics.h" #include "libs/errno.h" #include "libs/libs.h" @@ -105,7 +107,8 @@ private: int channels_num = 0; int volume[12] = {}; - SDL_AudioStream* stream = nullptr; + SDL_AudioStream* stream = nullptr; + Controller::DualSenseHaptics::Stream* haptics = nullptr; }; struct PortIn { @@ -268,6 +271,8 @@ bool Audio::OpenSdlDevice(PortOut* port) { void Audio::CloseSdlDevice(PortOut* port) { EXIT_IF(port == nullptr); + Controller::DualSenseHaptics::Close(port->haptics); + port->haptics = nullptr; if (port->stream != nullptr) { SDL_DestroyAudioStream(port->stream); @@ -435,7 +440,9 @@ Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, For port.volume[i] = 32768; } - if (type != AUDIO_OUT_PORT_TYPE_VIBRATION) { + if (type == AUDIO_OUT_PORT_TYPE_VIBRATION) { + port.haptics = Controller::DualSenseHaptics::Open(freq); + } else { OpenSdlDevice(&port); } @@ -547,7 +554,15 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking for (uint32_t i = 0; i < num; i++) { auto& port = m_out_ports[params[i].handle.GetId()]; - QueueSdlAudio(&port, params[i].data, blocking); + if (port.type == AUDIO_OUT_PORT_TYPE_VIBRATION) { + // Haptics never pace output; keep the stream alive against close and volume changes. + Common::LockGuard lock(m_mutex); + Controller::DualSenseHaptics::Queue( + port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num, + static_cast(port.channels_num), FormatIsFloat(port.format), port.volume); + } else { + QueueSdlAudio(&port, params[i].data, blocking); + } } for (uint32_t i = 0; i < num; i++) { diff --git a/src/libs/controller.cpp b/src/libs/controller.cpp index b1c24063..9e11d3b6 100644 --- a/src/libs/controller.cpp +++ b/src/libs/controller.cpp @@ -8,6 +8,7 @@ #include "common/stringUtils.h" #include "common/threads.h" #include "kernel/pthread.h" +#include "libs/dualSenseHaptics.h" #include "libs/errno.h" #include "libs/libs.h" #include "libs/padData.h" @@ -113,6 +114,7 @@ public: void ReleaseHostPads(); void GetConnectionInfo(bool* flag, int* count); void SetVibration(uint8_t large_motor, uint8_t small_motor); + int GetActiveControllerId(); void SetLightBar(uint8_t r, uint8_t g, uint8_t b); bool SetTriggerEffect(const PadTriggerEffectParam& param); void ReadState(ControllerState* state, bool* flag, int* count); @@ -518,6 +520,7 @@ void GameController::ResetInputState() { void GameController::ReleaseHostPads() { Common::LockGuard lock(m_mutex); + DualSenseHaptics::Shutdown(); std::vector pads; for (const auto id: m_connected_ids) { @@ -551,6 +554,9 @@ void GameController::ReleaseHostPads() { void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) { Common::LockGuard lock(m_mutex); + if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor)) { + return; + } if (m_active_id == HOST_INPUT_CONTROLLER_ID) { return; @@ -568,6 +574,11 @@ void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) { } } +int GameController::GetActiveControllerId() { + Common::LockGuard lock(m_mutex); + return m_active_id; +} + void GameController::SetLightBar(uint8_t r, uint8_t g, uint8_t b) { Common::LockGuard lock(m_mutex); if (auto* pad = SDL_GetGamepadFromID(static_cast(m_active_id)); @@ -692,6 +703,10 @@ void ResetInputState() { g_controller->ResetInputState(); } +int GetActiveControllerId() { + return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1; +} + int KYTY_SYSV_ABI PadInit() { PRINT_NAME(); diff --git a/src/libs/controller.h b/src/libs/controller.h index bdbcb975..217e23cd 100644 --- a/src/libs/controller.h +++ b/src/libs/controller.h @@ -73,6 +73,7 @@ void SetRightStick(int id, int x, int y); void SetTouchPad(int id, int finger, bool down, float x, float y); void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us); void ResetInputState(); +int GetActiveControllerId(); int KYTY_SYSV_ABI PadInit(); int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param); diff --git a/src/libs/dualSenseHaptics.cpp b/src/libs/dualSenseHaptics.cpp new file mode 100644 index 00000000..b092f720 --- /dev/null +++ b/src/libs/dualSenseHaptics.cpp @@ -0,0 +1,205 @@ +#include "libs/dualSenseHaptics.h" + +#include "common/logging/log.h" +#include "common/threads.h" + +#include +#include +#include +#include + +namespace Libs::Controller::DualSenseHaptics { + +struct Stream { + uint32_t freq = 0; + SDL_AudioStream* sdl = nullptr; + SDL_AudioDeviceID device = 0; + uint64_t next_check = 0; + std::vector buffer; +}; + +namespace { + +constexpr int FRAME_BYTES = 4 * sizeof(float); +Common::Mutex g_mutex; +int g_controller = -1; +int g_playback_streams = 0; +uint8_t g_large_motor = 0, g_small_motor = 0; +uint64_t g_rumble_until = 0, g_haptics_until = 0; + +void ApplyRumble() { // Caller holds g_mutex; no calls into Audio or Controller. + SDL_LockJoysticks(); + if (auto* pad = SDL_GetGamepadFromID(static_cast(g_controller)); + pad != nullptr) { + const auto now = SDL_GetTicks(); + const bool muted = g_haptics_until > now || g_rumble_until <= now; + (void)SDL_RumbleGamepad( + pad, muted ? 0 : g_large_motor * 0x101U, muted ? 0 : g_small_motor * 0x101U, + g_rumble_until > now ? static_cast(g_rumble_until - now) : 0); + } + SDL_UnlockJoysticks(); +} + +void SelectController(int controller) { + if (g_controller != controller) { + g_haptics_until = 0; + ApplyRumble(); + g_controller = controller; + g_large_motor = g_small_motor = 0; + g_rumble_until = 0; + } +} + +void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) { + Common::LockGuard lock(g_mutex); + if (g_haptics_until != 0 && SDL_GetTicks() >= g_haptics_until) { + g_haptics_until = 0; + ApplyRumble(); + } +} + +void CloseDevice(Stream* stream) { + if (stream->sdl != nullptr) { + // Destroy outside g_mutex: SDL waits for any running get callback. + SDL_DestroyAudioStream(stream->sdl); + stream->sdl = nullptr; + Common::LockGuard lock(g_mutex); + if (--g_playback_streams == 0 && g_haptics_until != 0) { + g_haptics_until = 0; + ApplyRumble(); + } + } +} + +SDL_AudioDeviceID FindDevice() { + int count = 0; + auto* devices = SDL_GetAudioPlaybackDevices(&count); + SDL_AudioDeviceID device = 0; + for (int i = 0; i < count && device == 0; i++) { + const char* name = SDL_GetAudioDeviceName(devices[i]); + SDL_AudioSpec spec {}; + if (name != nullptr && SDL_strcasestr(name, "DualSense") != nullptr && + SDL_GetAudioDeviceFormat(devices[i], &spec, nullptr) && spec.channels == 4) { + device = devices[i]; + } + } + SDL_free(devices); + return device; +} + +} // namespace + +Stream* Open(uint32_t freq) { + if (freq == 0 || !SDL_InitSubSystem(SDL_INIT_AUDIO)) { + return nullptr; + } + return new Stream {.freq = freq}; +} + +void Close(Stream* stream) { + if (stream != nullptr) { + CloseDevice(stream); + delete stream; + SDL_QuitSubSystem(SDL_INIT_AUDIO); + } +} + +void Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels, + bool is_float, const int* volume) { + if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) { + return; + } + if (SDL_GetGamepadTypeForID(static_cast(controller)) != SDL_GAMEPAD_TYPE_PS5) { + CloseDevice(stream); + return; + } + const auto now = SDL_GetTicks(); + if (now >= stream->next_check) { + stream->next_check = now + 2000; + const auto device = FindDevice(); + if (stream->device != device || stream->sdl == nullptr) { + CloseDevice(stream); + stream->device = device; + if (device != 0) { + const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast(stream->freq)}; + auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr); + SDL_AudioSpec actual {}; + if (sdl != nullptr && + SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) && + actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl)) { + stream->sdl = sdl; + { + Common::LockGuard lock(g_mutex); + g_playback_streams++; + } + LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device)); + } else { + SDL_DestroyAudioStream(sdl); + LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError()); + } + } + } + } + if (stream->sdl == nullptr) { + return; + } + stream->buffer.assign(static_cast(frames) * 4, 0.0f); + bool audible = false; + for (uint32_t frame = 0; frame < frames; frame++) { + for (uint32_t ch = 0; ch < 2; ch++) { + const auto src_ch = channels == 1 ? 0 : ch; + const auto index = static_cast(frame) * channels + src_ch; + float value = is_float ? static_cast(data)[index] + : static_cast(data)[index] / 32768.0f; + value *= volume[src_ch] / 32768.0f; + stream->buffer[static_cast(frame) * 4 + ch + 2] = value; + audible |= std::fabs(value) > 1.0f / 1024; + } + } + auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl)); + if (queued > static_cast(stream->freq * FRAME_BYTES * 80 / 1000)) { + SDL_ClearAudioStream(stream->sdl); + queued = 0; + } + const int bytes = static_cast(frames * FRAME_BYTES); + if (audible) { + Common::LockGuard lock(g_mutex); + if (SDL_GetGamepadTypeForID(static_cast(controller)) != + SDL_GAMEPAD_TYPE_PS5) { + return; + } + SelectController(controller); + const auto playback_now = SDL_GetTicks(); + const bool starting = g_haptics_until <= playback_now; + const auto duration = (queued + bytes) * 1000ULL / (stream->freq * FRAME_BYTES); + g_haptics_until = + std::max(g_haptics_until, playback_now + std::max(duration, 250)); + if (starting) { + ApplyRumble(); + } + } + if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) { + CloseDevice(stream); + } +} + +bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) { + if (SDL_GetGamepadTypeForID(static_cast(controller)) != SDL_GAMEPAD_TYPE_PS5) { + return false; + } + Common::LockGuard lock(g_mutex); + SelectController(controller); + g_large_motor = large_motor; + g_small_motor = small_motor; + g_rumble_until = SDL_GetTicks() + 0xffff; + ApplyRumble(); + return true; +} + +void Shutdown() { + Common::LockGuard lock(g_mutex); + g_rumble_until = 0; + SelectController(-1); +} + +} // namespace Libs::Controller::DualSenseHaptics diff --git a/src/libs/dualSenseHaptics.h b/src/libs/dualSenseHaptics.h new file mode 100644 index 00000000..e3b546f2 --- /dev/null +++ b/src/libs/dualSenseHaptics.h @@ -0,0 +1,20 @@ +#ifndef EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_ +#define EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_ + +#include + +namespace Libs::Controller::DualSenseHaptics { + +struct Stream; + +Stream* Open(uint32_t freq); +void Close(Stream* stream); +void Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels, + bool is_float, const int* volume); +// Returns false for other gamepad types, which retain the normal rumble path. +bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor); +void Shutdown(); + +} // namespace Libs::Controller::DualSenseHaptics + +#endif // EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_ diff --git a/tests/PadHapticsTests.cpp b/tests/PadHapticsTests.cpp new file mode 100644 index 00000000..f4c690b3 --- /dev/null +++ b/tests/PadHapticsTests.cpp @@ -0,0 +1,321 @@ +// 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"); +}