diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 65608a6e..cdc8220e 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -2,10 +2,31 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) add_subdirectory(cpuinfo) -# The Windows guest red-zone patcher needs a decoder/encoder and a small -# trampoline assembler. Pin the same revisions used by shadPS4's implementation. include(FetchContent) +# The Bluetooth DualSense transports speaker audio as fixed-size Opus packets. +set(kyty_build_testing_saved "${BUILD_TESTING}") +set(BUILD_TESTING OFF) +set(OPUS_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE) +set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF CACHE BOOL "" FORCE) +set(OPUS_INSTALL_CMAKE_CONFIG_MODULE OFF CACHE BOOL "" FORCE) +if(MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang") + # Opus assumes MSVC accepts SSE4.1 intrinsics without per-file flags, but + # clang-cl rejects them in its baseline x64 files. Its AVX2 dispatch also + # depends on the SSE4.1 declarations, so retain the x64 SSE2 baseline only. + set(OPUS_X86_MAY_HAVE_SSE4_1 OFF CACHE BOOL "" FORCE) + set(OPUS_X86_MAY_HAVE_AVX2 OFF CACHE BOOL "" FORCE) +endif() +FetchContent_Declare(opus + GIT_REPOSITORY https://github.com/xiph/opus.git + GIT_TAG ddbe48383984d56acd9e1ab6a090c54ca6b735a6 + GIT_SHALLOW FALSE) +FetchContent_MakeAvailable(opus) +set(BUILD_TESTING "${kyty_build_testing_saved}") +install(FILES "${opus_SOURCE_DIR}/COPYING" DESTINATION "licenses/opus") + +# The Windows guest red-zone patcher needs a decoder/encoder and a small +# trampoline assembler. Pin the same revisions used by shadPS4's implementation. set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE) FetchContent_Declare( xbyak diff --git a/CMakeLists.txt b/CMakeLists.txt index 09cd4d72..97da78dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -332,7 +332,7 @@ if (KYTY_CLANG_CL) set_source_files_properties(${kyty_emulator_src} PROPERTIES COMPILE_FLAGS "-Wno-pragma-pack -Wno-deprecated-declarations -D_TIMESPEC_DEFINED") endif() -set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak) +set(kyty_emulator_link_libraries common Vulkan::Headers spirv-tools-opt spirv-tools SDL3::SDL3-static imgui xxhash FFmpeg::ffmpeg fmt::fmt nlohmann_json::nlohmann_json LibAtrac9 kyty_git_version Zydis::Zydis xbyak::xbyak opus) # Linux system libraries. if(LINUX) @@ -495,7 +495,7 @@ 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_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static opus) target_include_directories(pad_haptics_tests PRIVATE ${inc_headers}) add_executable(save_data_memory_tests EXCLUDE_FROM_ALL diff --git a/src/libs/audio.cpp b/src/libs/audio.cpp index e653c52a..216084b9 100644 --- a/src/libs/audio.cpp +++ b/src/libs/audio.cpp @@ -428,6 +428,10 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) { } Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, Format format) { + if (freq == 0) { + return Id::Invalid(); + } + Common::LockGuard lock(m_mutex); for (int id = 0; id < OUT_PORTS_MAX; id++) { @@ -552,12 +556,15 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking auto& port = m_out_ports[params[i].handle.GetId()]; uint64_t controller_queued_us = 0; + bool controller_uses_bluetooth = false; if (port.haptics != nullptr) { // Keep the stream alive against close and volume changes. Common::LockGuard lock(m_mutex); controller_queued_us = Controller::DualSenseHaptics::Queue( port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num, static_cast(port.channels_num), FormatIsFloat(port.format), port.volume); + controller_uses_bluetooth = + Controller::DualSenseHaptics::UsesBluetooth(port.haptics); } if (controller_queued_us == 0) { // No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main @@ -568,12 +575,16 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking } } else { port.queue_primed = false; - if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK) { + // Bluetooth HID has its own sender. Let the sample clock pace a batch + // without another audio device, instead of waiting on the HID queue. + if (port.type == AUDIO_OUT_PORT_TYPE_PADSPK && !controller_uses_bluetooth) { any_device = true; if (blocking && controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) { - deadline = - std::max(deadline, LibKernel::KernelGetProcessTime() + - controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US); + // Limit a stalled USB endpoint to one audio block of waiting. + const uint64_t block_us = 1000000ULL * port.samples_num / port.freq; + deadline = std::max(deadline, LibKernel::KernelGetProcessTime() + + std::min(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US, + block_us)); } } } @@ -842,6 +853,9 @@ int KYTY_SYSV_ABI AudioOutOpen(int user_id, int type, int index, uint32_t len, u if (!audio_out_port_type_is_valid(type)) { return AUDIO_OUT_ERROR_INVALID_PORT_TYPE; } + if (freq == 0) { + return AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ; + } EXIT_NOT_IMPLEMENTED(index != 0); Audio::Format format = Audio::Format::Unknown; diff --git a/src/libs/dualSenseBluetooth.cpp b/src/libs/dualSenseBluetooth.cpp new file mode 100644 index 00000000..fa21e965 --- /dev/null +++ b/src/libs/dualSenseBluetooth.cpp @@ -0,0 +1,467 @@ +#include "libs/dualSenseBluetooth.h" + +#include "common/logging/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Libs::Controller::DualSenseBluetooth { + +namespace { +// Report 0x39 and its sub-packets are described by awalol/DS5Dongle (MIT): +// https://github.com/awalol/DS5Dongle +// Each report plays two 32-frame stereo haptic blocks at 3 kHz (21.333 ms). +constexpr uint64_t PERIOD_NS = 64000000000ULL / 3000; +constexpr int HAPTIC_RATE = 3000; +constexpr int HAPTIC_REPORT_FLOATS = 2 * 32 * 2; +// Two 480-frame Opus packets occupy the same 21.333 ms slot. Resampling +// 48 kHz input to 45 kHz before encoding at 48 kHz compensates for playback. +constexpr int SPEAKER_RATE = 45000; +constexpr int OPUS_FRAMES = 480; +constexpr int OPUS_BYTES = 200; +constexpr size_t REPORT_BYTES = 547; +constexpr int MAX_QUEUE_MS = 80; +constexpr uint64_t COALESCE_NS = 1000000; + +struct Session; +std::mutex g_mutex; +std::condition_variable g_wake; +std::map> g_sessions; +SDL_Thread* g_thread = nullptr; +bool g_stopping = false; +void (*g_tick)() = nullptr; + +template +std::string Address(const Char* serial) { + std::string address; + if (serial == nullptr) { + return address; + } + for (; *serial != 0; ++serial) { + if (*serial >= '0' && *serial <= '9') { + address.push_back(static_cast(*serial)); + } else if (*serial >= 'a' && *serial <= 'f') { + address.push_back(static_cast(*serial)); + } else if (*serial >= 'A' && *serial <= 'F') { + address.push_back(static_cast(*serial - 'A' + 'a')); + } else if (*serial != ':' && *serial != '-') { + return {}; + } + } + return address.size() == 12 ? address : std::string {}; +} + +SDL_hid_device* OpenDevice(int controller) { + SDL_LockJoysticks(); + auto* pad = SDL_GetGamepadFromID(static_cast(controller)); + const bool wireless = pad != nullptr && SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5 && + SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRELESS; + const auto vendor = wireless ? SDL_GetGamepadVendor(pad) : 0; + const auto product = wireless ? SDL_GetGamepadProduct(pad) : 0; + const auto address = wireless ? Address(SDL_GetGamepadSerial(pad)) : std::string {}; + SDL_UnlockJoysticks(); + if (vendor != 0x054c || (product != 0x0ce6 && product != 0x0df2) || address.empty()) { + return nullptr; + } + auto* devices = SDL_hid_enumerate(vendor, product); + std::string path; + bool ambiguous = false; + for (auto* device = devices; device != nullptr; device = device->next) { + if (device->bus_type == SDL_HID_API_BUS_BLUETOOTH && device->path != nullptr && + Address(device->serial_number) == address && + (device->usage_page == 0 || (device->usage_page == 1 && device->usage == 5))) { + if (!path.empty()) { + ambiguous = true; + break; + } + path = device->path; + } + } + SDL_hid_free_enumeration(devices); + return !ambiguous && !path.empty() ? SDL_hid_open_path(path.c_str()) : nullptr; +} + +void FinishReport(std::array& report) { + const uint8_t prefix = 0xa2; + auto crc = SDL_crc32(0, &prefix, 1); + crc = SDL_crc32(crc, report.data(), report.size() - 4); + for (size_t i = 0; i < 4; i++) { + report[report.size() - 4 + i] = static_cast(crc >> (8 * i)); + } +} + +struct Session { + SDL_hid_device* hid = nullptr; + OpusEncoder* encoder = nullptr; + std::vector streams; + std::mutex io_mutex; + std::atomic active {true}; + std::atomic failed {false}; + std::atomic generation {0}; + uint64_t next_send = 0; + uint64_t pending_deadline = 0; + uint8_t sequence = 0; + uint8_t counter = 0; + + ~Session() { + if (encoder != nullptr) { + opus_encoder_destroy(encoder); + } + if (hid != nullptr) { + SDL_hid_close(hid); + } + } + + bool CreateEncoder() { + if (encoder != nullptr) { + return true; + } + int error = OPUS_OK; + encoder = opus_encoder_create(48000, 2, OPUS_APPLICATION_AUDIO, &error); + if (encoder == nullptr || error != OPUS_OK) { + return false; + } + if (opus_encoder_ctl(encoder, OPUS_SET_BITRATE(OPUS_BYTES * 8 * 100)) == OPUS_OK && + opus_encoder_ctl(encoder, OPUS_SET_VBR(0)) == OPUS_OK && + opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(0)) == OPUS_OK) { + return true; + } + opus_encoder_destroy(encoder); + encoder = nullptr; + return false; + } +}; +} // namespace + +struct Stream { + std::shared_ptr session; + SDL_AudioStream* audio = nullptr; + uint32_t freq = 0; + bool speaker = false; +}; + +namespace { +template +void Mix(SDL_AudioStream* stream, std::array& mixed) { + std::array pcm {}; + const int bytes = SDL_GetAudioStreamData(stream, pcm.data(), sizeof(pcm)); + for (int i = 0; i < std::max(0, bytes) / static_cast(sizeof(float)); i++) { + mixed[i] += pcm[i]; + } +} + +struct PendingReport { + std::shared_ptr session; + uint64_t generation; + std::array data {}; + std::array audio {}; + bool speaker = false; +}; + +bool PrepareReport(Session& session, PendingReport& pending, uint64_t now) { + bool has_data = false; + bool ready = false; + bool speaker_ready = false; + for (const auto* stream: session.streams) { + const bool available = SDL_GetAudioStreamAvailable(stream->audio) > 0; + // Both payloads cover 64 / 3000 seconds. Measure input duration so SDL's + // resampler lookahead does not make a complete block appear partial. + const auto frames = + (static_cast(stream->freq) * (HAPTIC_REPORT_FLOATS / 2) + HAPTIC_RATE - 1) / + HAPTIC_RATE; + const bool full = available && SDL_GetAudioStreamQueued(stream->audio) >= + static_cast(frames * 2 * sizeof(float)); + has_data |= available; + ready |= full; + speaker_ready |= stream->speaker && full; + pending.speaker |= stream->speaker; + } + if (!has_data) { + session.pending_deadline = 0; + return false; + } + if (!ready || (!speaker_ready && (pending.speaker || session.next_send == 0))) { + // Accumulate short blocks for at most one report period. A full stream + // must not wait on a partial peer; only briefly coalesce haptics with + // speaker audio arriving in the same emulated batch. + const auto deadline = now + (ready ? COALESCE_NS : PERIOD_NS); + session.pending_deadline = + session.pending_deadline == 0 ? deadline : std::min(session.pending_deadline, deadline); + if (now < session.pending_deadline) { + return false; + } + } + session.pending_deadline = 0; + std::array haptics {}; + for (auto* stream: session.streams) { + if (stream->speaker) { + Mix(stream->audio, pending.audio); + } else { + Mix(stream->audio, haptics); + } + } + auto& report = pending.data; + report[0] = 0x39; + report[1] = static_cast(session.sequence << 4); + session.sequence = (session.sequence + 1) & 0x0f; + report[2] = 0x91; + report[3] = 6; + report[4] = 0x7e; + std::fill_n(report.begin() + 5, 4, 48); + session.counter += 2; + report[9] = session.counter; + report[10] = 0xd2; + report[11] = 64; + for (size_t i = 0; i < haptics.size(); i++) { + const auto value = std::isfinite(haptics[i]) ? haptics[i] : 0.0f; + report[12 + i] = static_cast( + static_cast(std::lround(std::clamp(value, -1.0f, 1.0f) * 127))); + } + if (pending.speaker) { + report[140] = 0xd3; + report[141] = OPUS_BYTES; + } + return true; +} + +bool EncodeReport(PendingReport& pending) { + if (!pending.session->active || pending.session->failed) { + return false; + } + if (pending.speaker) { + for (auto& sample: pending.audio) { + sample = std::isfinite(sample) ? std::clamp(sample, -1.0f, 1.0f) : 0.0f; + } + for (int packet = 0; packet < 2; packet++) { + if (opus_encode_float(pending.session->encoder, + pending.audio.data() + packet * OPUS_FRAMES * 2, OPUS_FRAMES, + pending.data.data() + 142 + packet * OPUS_BYTES, + OPUS_BYTES) != OPUS_BYTES) { + pending.session->failed = true; + return false; + } + } + } + FinishReport(pending.data); + return true; +} + +std::vector Prepare(uint64_t now) { + std::vector pending; + { + std::lock_guard lock(g_mutex); + for (auto& [id, session]: g_sessions) { + if (session->failed || now < session->next_send) { + continue; + } + PendingReport report {session, session->generation.load()}; + if (PrepareReport(*session, report, now)) { + pending.push_back(std::move(report)); + // Regain timing after a late HID write without sending reports in a + // burst. Reset the cadence only on the first report. + session->next_send = session->next_send == 0 + ? now + PERIOD_NS + : std::max(session->next_send + PERIOD_NS, + now + PERIOD_NS / 2); + } + } + } + // Opus encoding can take milliseconds; the emulation audio thread must be able + // to queue its next block while this work runs. + pending.erase(std::remove_if(pending.begin(), pending.end(), + [](PendingReport& report) { return !EncodeReport(report); }), + pending.end()); + return pending; +} + +void Send(const std::vector& pending) { + for (const auto& report: pending) { + auto& session = *report.session; + std::lock_guard io_lock(session.io_mutex); + if (!session.active || session.failed || report.generation != session.generation) { + continue; + } + if (SDL_hid_write(session.hid, report.data.data(), report.data.size()) != + static_cast(report.data.size())) { + session.failed = true; + LOGF("DualSenseBluetooth: audio HID write failed: %s\n", SDL_GetError()); + } + } +} + +int SDLCALL Sender(void*) { + std::unique_lock lock(g_mutex); + while (!g_stopping) { + auto tick = g_tick; + lock.unlock(); + auto pending = Prepare(SDL_GetTicksNS()); + Send(pending); + if (tick != nullptr) { + tick(); + } + lock.lock(); + if (g_sessions.empty()) { + g_wake.wait(lock, [] { return g_stopping || !g_sessions.empty(); }); + } else { + const auto now = SDL_GetTicksNS(); + uint64_t wait_ns = 10000000; // Check rumble expiry at most 10 ms late. + for (const auto& [id, session]: g_sessions) { + if (session->next_send > now) { + wait_ns = std::min(wait_ns, session->next_send - now); + } + if (session->pending_deadline > now) { + wait_ns = std::min(wait_ns, session->pending_deadline - now); + } + } + g_wake.wait_for(lock, std::chrono::nanoseconds(wait_ns)); + } + } + return 0; +} +} // namespace + +Stream* Open(uint32_t freq, bool speaker, int controller, void (*tick)()) { + if (freq == 0 || freq > 192000) { + return nullptr; + } + std::lock_guard lock(g_mutex); + if (g_stopping) { + return nullptr; + } + auto it = g_sessions.find(controller); + std::shared_ptr session; + if (it != g_sessions.end() && !it->second->failed) { + session = it->second; + } else { + session = std::make_shared(); + session->hid = OpenDevice(controller); + if (session->hid == nullptr) { + return nullptr; + } + } + if (speaker && !session->CreateEncoder()) { + return nullptr; + } + const SDL_AudioSpec source {SDL_AUDIO_F32, 2, static_cast(freq)}; + const SDL_AudioSpec target {SDL_AUDIO_F32, 2, speaker ? SPEAKER_RATE : HAPTIC_RATE}; + auto* audio = SDL_CreateAudioStream(&source, &target); + if (audio == nullptr) { + return nullptr; + } + if (g_thread == nullptr) { + g_thread = SDL_CreateThread(Sender, "DualSense Bluetooth audio", nullptr); + if (g_thread == nullptr) { + SDL_DestroyAudioStream(audio); + return nullptr; + } + } + g_tick = tick; + auto* stream = new Stream {session, audio, freq, speaker}; + session->streams.push_back(stream); + session->pending_deadline = 0; + g_sessions[controller] = session; + g_wake.notify_one(); + return stream; +} + +uint64_t Queue(Stream* stream, const float* stereo, uint32_t frames) { + if (stream == nullptr || stereo == nullptr || frames == 0) { + return 0; + } + std::lock_guard lock(g_mutex); + if (!stream->session->active || stream->session->failed) { + return 0; + } + const uint32_t max_frames = stream->freq * MAX_QUEUE_MS / 1000; + if (frames > max_frames) { + stereo += static_cast(frames - max_frames) * 2; + frames = max_frames; + } + int queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio)); + const int bytes = static_cast(frames * 2 * sizeof(float)); + if (queued + bytes > static_cast(max_frames * 2 * sizeof(float))) { + // Discard complete old report slots, preserving the newest queued audio. + // Clearing the entire stream here creates an audible gap after a brief + // Windows Bluetooth write stall. + std::array discarded {}; + const int discard_bytes = stream->speaker + ? static_cast(sizeof(discarded)) + : static_cast(HAPTIC_REPORT_FLOATS * sizeof(float)); + const int target = static_cast(max_frames * 2 * sizeof(float)) - bytes; + while (queued > target) { + if (SDL_GetAudioStreamData(stream->audio, discarded.data(), discard_bytes) <= 0) { + SDL_ClearAudioStream(stream->audio); + break; + } + queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio)); + } + } + queued = std::max(0, SDL_GetAudioStreamQueued(stream->audio)); + if (!SDL_PutAudioStreamData(stream->audio, stereo, bytes)) { + return 0; + } + g_wake.notify_one(); + return static_cast(queued + bytes) * 1000000 / (stream->freq * 2 * sizeof(float)); +} + +void Close(Stream* stream) { + if (stream == nullptr) { + return; + } + auto session = stream->session; + { + std::lock_guard lock(g_mutex); + auto& streams = session->streams; + streams.erase(std::remove(streams.begin(), streams.end(), stream), streams.end()); + session->generation++; + session->pending_deadline = 0; + if (streams.empty()) { + session->active = false; + for (auto it = g_sessions.begin(); it != g_sessions.end(); ++it) { + if (it->second == session) { + g_sessions.erase(it); + break; + } + } + } + SDL_DestroyAudioStream(stream->audio); + delete stream; + } + // Drain a write already in flight before the caller changes speaker routing. + std::lock_guard io_lock(session->io_mutex); +} + +void Shutdown() { + SDL_Thread* thread; + { + std::lock_guard lock(g_mutex); + g_stopping = true; + for (auto& [id, session]: g_sessions) { + session->active = false; + } + thread = g_thread; + g_wake.notify_one(); + } + if (thread != nullptr) { + SDL_WaitThread(thread, nullptr); + } + std::lock_guard lock(g_mutex); + g_sessions.clear(); + g_thread = nullptr; + g_tick = nullptr; + g_stopping = false; +} + +} // namespace Libs::Controller::DualSenseBluetooth diff --git a/src/libs/dualSenseBluetooth.h b/src/libs/dualSenseBluetooth.h new file mode 100644 index 00000000..7b2f4291 --- /dev/null +++ b/src/libs/dualSenseBluetooth.h @@ -0,0 +1,14 @@ +#ifndef KYTY_LIBS_DUALSENSE_BLUETOOTH_H_ +#define KYTY_LIBS_DUALSENSE_BLUETOOTH_H_ + +#include + +namespace Libs::Controller::DualSenseBluetooth { +struct Stream; +Stream* Open(uint32_t frequency, bool speaker, int controller, void (*tick)()); +uint64_t Queue(Stream* stream, const float* samples, uint32_t frames); +void Close(Stream* stream); +void Shutdown(); +} // namespace Libs::Controller::DualSenseBluetooth + +#endif diff --git a/src/libs/dualSenseHaptics.cpp b/src/libs/dualSenseHaptics.cpp index 95a82dea..7e89626b 100644 --- a/src/libs/dualSenseHaptics.cpp +++ b/src/libs/dualSenseHaptics.cpp @@ -2,6 +2,7 @@ #include "common/logging/log.h" #include "common/threads.h" +#include "libs/dualSenseBluetooth.h" #include #include @@ -13,18 +14,20 @@ namespace Libs::Controller::DualSenseHaptics { struct Stream { - uint32_t freq = 0; - bool speaker = false; - int controller = -1; - SDL_AudioStream* sdl = nullptr; - SDL_AudioDeviceID device = 0; - uint64_t next_check = 0; - std::vector buffer; + uint32_t freq = 0; + bool speaker = false; + int controller = -1; + SDL_AudioStream* sdl = nullptr; + DualSenseBluetooth::Stream* bluetooth = nullptr; + SDL_AudioDeviceID device = 0; + uint64_t next_check = 0; + std::vector buffer; }; namespace { -constexpr int FRAME_BYTES = 4 * sizeof(float); +constexpr int FRAME_BYTES = 4 * sizeof(float); +constexpr uint32_t MAX_QUEUE_MS = 80; Common::Mutex g_mutex; int g_controller = -1; int g_haptics_streams = 0; @@ -107,11 +110,22 @@ void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) { } } +void UpdateBluetoothRumble() { + UpdateRumble(nullptr, nullptr, 0, 0); +} + void CloseDevice(Stream* stream) { + const bool had_device = stream->sdl != nullptr || stream->bluetooth != nullptr; + if (stream->bluetooth != nullptr) { + DualSenseBluetooth::Close(stream->bluetooth); + stream->bluetooth = nullptr; + } if (stream->sdl != nullptr) { // Destroy outside g_mutex: SDL waits for any running get callback. SDL_DestroyAudioStream(stream->sdl); stream->sdl = nullptr; + } + if (had_device) { Common::LockGuard lock(g_mutex); if (!stream->speaker && --g_haptics_streams == 0 && g_haptics_until != 0) { g_haptics_until = 0; @@ -127,26 +141,34 @@ void CloseDevice(Stream* stream) { } } +bool IsWireless(int controller) { + SDL_LockJoysticks(); + auto* pad = SDL_GetGamepadFromID(static_cast(controller)); + const bool wireless = + pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRELESS; + SDL_UnlockJoysticks(); + return wireless; +} + bool CanUseDevice(int controller) { // SDL exposes no portable association between a gamepad and its USB audio endpoint. // Require one wired DualSense; guessing with multiple pads could play on the wrong one. SDL_LockJoysticks(); - auto* pad = SDL_GetGamepadFromID(static_cast(controller)); - bool usable = - pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRED; - int count = 0; - auto* pads = SDL_GetGamepads(&count); - int dualsense_count = 0; - bool found = false; + int count = 0; + auto* pads = SDL_GetGamepads(&count); + int wired_dualsense_count = 0; + bool found = false; for (int i = 0; i < count; i++) { - if (SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5) { - dualsense_count++; + auto* candidate = SDL_GetGamepadFromID(pads[i]); + if (candidate != nullptr && SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5 && + SDL_GetGamepadConnectionState(candidate) == SDL_JOYSTICK_CONNECTION_WIRED) { + wired_dualsense_count++; found |= pads[i] == static_cast(controller); } } SDL_free(pads); SDL_UnlockJoysticks(); - return usable && found && dualsense_count == 1; + return found && wired_dualsense_count == 1; } SDL_AudioDeviceID FindDevice() { @@ -169,32 +191,42 @@ SDL_AudioDeviceID FindDevice() { return device; } -void RefreshDevice(Stream* stream, int controller) { - if (stream->controller != controller) { +void RefreshDevice(Stream* stream, int controller, bool wireless) { + if (stream->controller != controller || (wireless && stream->sdl != nullptr) || + (!wireless && stream->bluetooth != nullptr)) { CloseDevice(stream); stream->controller = controller; stream->next_check = 0; } const auto now = SDL_GetTicks(); - if (now < stream->next_check) { + if (stream->bluetooth != nullptr || now < stream->next_check) { return; } stream->next_check = now + 2000; - const auto device = FindDevice(); + const auto device = wireless ? 0 : FindDevice(); if (stream->device == device && stream->sdl != nullptr) { return; } CloseDevice(stream); stream->device = device; - if (device == 0) { + if (!wireless && device == 0) { return; } - const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast(stream->freq)}; - auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr); - SDL_AudioSpec actual {}; - bool ready = sdl != nullptr && - SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) && - actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl); + SDL_AudioStream* sdl = nullptr; + DualSenseBluetooth::Stream* bluetooth = nullptr; + bool ready; + if (wireless) { + bluetooth = DualSenseBluetooth::Open(stream->freq, stream->speaker, controller, + UpdateBluetoothRumble); + ready = bluetooth != nullptr; + } else { + const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast(stream->freq)}; + SDL_AudioSpec actual {}; + sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr); + ready = sdl != nullptr && + SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) && + actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl); + } if (ready) { Common::LockGuard lock(g_mutex); if (stream->speaker) { @@ -207,14 +239,56 @@ void RefreshDevice(Stream* stream, int controller) { } } if (ready) { - stream->sdl = sdl; - LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device)); + stream->sdl = sdl; + stream->bluetooth = bluetooth; + if (!wireless) { + LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device)); + } } else { + DualSenseBluetooth::Close(bluetooth); SDL_DestroyAudioStream(sdl); - LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError()); + if (!wireless) { + LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError()); + } } } +uint64_t QueueUsbAudio(Stream* stream, uint32_t frames) { + auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl)); + const int bytes = static_cast(frames * FRAME_BYTES); + const int max_bytes = + static_cast(static_cast(stream->freq) * FRAME_BYTES * MAX_QUEUE_MS / 1000); + if (queued + bytes > max_bytes) { + // Keep the newest sound after a brief device stall. Clearing the entire + // stream produces a gap that is much longer than one audio block. + SDL_AudioSpec target {}; + std::array discarded {}; + if (SDL_GetAudioStreamFormat(stream->sdl, nullptr, &target)) { + const int frame_bytes = SDL_AUDIO_FRAMESIZE(target); + if (frame_bytes > 0) { + const int chunk_frames = + std::min(static_cast(discarded.size()) / frame_bytes, + static_cast((static_cast(frames) * target.freq + + stream->freq - 1) / + stream->freq)); + while (queued + bytes > max_bytes && chunk_frames > 0 && + SDL_GetAudioStreamData(stream->sdl, discarded.data(), + chunk_frames * frame_bytes) > 0) { + queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl)); + } + } + } + if (queued + bytes > max_bytes) { + SDL_ClearAudioStream(stream->sdl); + queued = 0; + } + } + if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) { + return 0; + } + return static_cast(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES); +} + } // namespace Stream* Open(uint32_t freq, bool speaker) { @@ -232,18 +306,30 @@ void Close(Stream* stream) { } } +bool UsesBluetooth(const Stream* stream) { + return stream != nullptr && stream->bluetooth != nullptr; +} + uint64_t 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 0; } - if (!CanUseDevice(controller)) { + const auto max_frames = + static_cast(static_cast(stream->freq) * MAX_QUEUE_MS / 1000); + const auto first_frame = frames > max_frames ? frames - max_frames : 0; + frames -= first_frame; + if (frames == 0) { + return 0; + } + const bool wireless = IsWireless(controller); + if (!wireless && !CanUseDevice(controller)) { CloseDevice(stream); stream->next_check = 0; // Look for the device as soon as a DualSense is active again. return 0; } - RefreshDevice(stream, controller); - if (stream->sdl == nullptr) { + RefreshDevice(stream, controller, wireless); + if (stream->sdl == nullptr && stream->bluetooth == nullptr) { return 0; } if (stream->speaker) { @@ -257,47 +343,42 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames return 0; } } - stream->buffer.assign(static_cast(frames) * 4, 0.0f); + const uint32_t output_channels = wireless ? 2 : 4; + const uint32_t channel_offset = wireless || stream->speaker ? 0 : 2; + stream->buffer.assign(static_cast(frames) * output_channels, 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; + const auto index = (static_cast(first_frame) + frame) * channels + src_ch; float value = is_float ? static_cast(data)[index] : static_cast(data)[index] / 32768.0f; value *= volume[src_ch] / 32768.0f; - // The speaker plays the right front channel; the actuators play the back ones. - stream->buffer[static_cast(frame) * 4 + ch + (stream->speaker ? 0 : 2)] = value; - audible |= std::fabs(value) > 1.0f / 1024; + // USB places the speaker in front and the actuators in back; Bluetooth takes stereo. + stream->buffer[static_cast(frame) * output_channels + ch + channel_offset] = + value; + audible |= std::isfinite(value) && 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); const auto queued_us = - static_cast(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES); + wireless ? DualSenseBluetooth::Queue(stream->bluetooth, stream->buffer.data(), frames) + : QueueUsbAudio(stream, frames); + if (queued_us == 0) { + CloseDevice(stream); + stream->next_check = SDL_GetTicks() + 2000; + return 0; + } if (audible && !stream->speaker) { Common::LockGuard lock(g_mutex); - if (SDL_GetGamepadTypeForID(static_cast(controller)) != - SDL_GAMEPAD_TYPE_PS5) { - return 0; - } SelectController(controller); - const auto playback_now = SDL_GetTicks(); - const bool starting = g_haptics_until <= playback_now; + const auto now = SDL_GetTicks(); + const bool starting = g_haptics_until <= now; g_haptics_until = - std::max(g_haptics_until, playback_now + std::max(queued_us / 1000, 250)); + std::max(g_haptics_until, now + std::max(queued_us / 1000, 250)); if (starting) { ApplyRumble(); } } - if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) { - CloseDevice(stream); - return 0; - } return queued_us; } @@ -315,6 +396,7 @@ bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) { } void Shutdown() { + DualSenseBluetooth::Shutdown(); Common::LockGuard lock(g_mutex); SelectController(-1); SelectSpeaker(-1); diff --git a/src/libs/dualSenseHaptics.h b/src/libs/dualSenseHaptics.h index e473e13d..6a2016b2 100644 --- a/src/libs/dualSenseHaptics.h +++ b/src/libs/dualSenseHaptics.h @@ -11,9 +11,11 @@ struct Stream; // ports on the actuators. Stream* Open(uint32_t freq, bool speaker); void Close(Stream* stream); +// True while this stream is routed through the asynchronous Bluetooth HID sender. +bool UsesBluetooth(const Stream* stream); // Returns how long the queued audio still plays, in microseconds, or 0 when no DualSense took it -// (the active pad has no usable USB audio device or its speaker cannot be routed). -// Multiple DualSenses or matching audio endpoints are ambiguous and are not selected. +// (the active pad has no usable controller audio device or its speaker cannot be routed). +// Ambiguous USB or Bluetooth endpoints are not selected. uint64_t 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. diff --git a/tests/AudioOutTimingTests.cpp b/tests/AudioOutTimingTests.cpp index cd867777..bcf65521 100644 --- a/tests/AudioOutTimingTests.cpp +++ b/tests/AudioOutTimingTests.cpp @@ -29,6 +29,8 @@ uint32_t oversleep = 0, processing = 0; std::vector sleeps; std::vector> streams; bool fail_open = false, fail_put = false, stalled = false; +bool pad_connected = false, pad_bluetooth = false; +uint64_t pad_queue_us = 0; int clears = 0; Stream& GetStream(SDL_AudioStream* stream) { @@ -136,11 +138,15 @@ int GetActiveControllerId() { namespace DualSenseHaptics { Stream* Open(uint32_t, bool) { - return nullptr; + static int pad_stream; + return pad_connected ? reinterpret_cast(&pad_stream) : nullptr; } void Close(Stream*) {} -uint64_t Queue(Stream*, int, const void*, uint32_t, uint32_t, bool, const int*) { - return 0; +bool UsesBluetooth(const Stream* stream) { + return stream != nullptr && pad_bluetooth; +} +uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*) { + return stream != nullptr ? pad_queue_us : 0; } } // namespace DualSenseHaptics } // namespace Libs::Controller @@ -170,6 +176,8 @@ struct Fixture { processing = oversleep = 0; clears = 0; fail_open = fail_put = stalled = false; + pad_connected = pad_bluetooth = false; + pad_queue_us = 0; sleeps.clear(); } @@ -336,6 +344,27 @@ void TestFailedQueueUsesFallbackClock() { Check(now - start == 16000, "repeated queue failures reset fallback pacing"); } +void TestControllerSpeakerPacing() { + Fixture f; + fail_open = true; // No PC audio device paces the pad speaker. + pad_connected = true; + pad_queue_us = 80000; + const auto speaker = f.Open(256, 4); + + pad_bluetooth = true; + f.Output(speaker); + const auto bluetooth_start = now; + f.Output(speaker); + Check(now - bluetooth_start >= 5333 && now - bluetooth_start <= 5334, + "Bluetooth speaker waited on its HID queue instead of the sample clock"); + + pad_bluetooth = false; + const auto usb_start = now; + f.Output(speaker); + Check(now - usb_start >= 5333 && now - usb_start <= 5334, + "stalled USB speaker waited longer than one audio block"); +} + void TestInvalidBatchSize() { Libs::Audio::AudioOut::AudioOutOutputParam param {}; for (const auto count: {0u, 33u}) { @@ -344,6 +373,16 @@ void TestInvalidBatchSize() { "invalid batch size was not rejected before accessing ports"); } } + +void TestZeroOutputFrequency() { + Fixture f; + Check(Libs::Audio::AudioOut::AudioOutOpen(0, 0, 0, 256, 0, 4) == + Libs::Audio::AUDIO_OUT_ERROR_INVALID_SAMPLE_FREQ, + "public output open accepted zero frequency"); + Check(!f.audio.AudioOutOpen(10, 256, 0, Audio::Format::FloatStereo).IsValid(), + "internal output open accepted zero frequency"); + Check(f.Open().ToInt() == 1, "rejected output port occupied a handle"); +} } // namespace int main() { @@ -354,7 +393,9 @@ int main() { TestSynchronizedBatchAndInactivePorts(); TestFallbackUsesEachPortsPeriod(); TestFailedQueueUsesFallbackClock(); + TestControllerSpeakerPacing(); TestInvalidBatchSize(); + TestZeroOutputFrequency(); Check(streams.empty(), "output stream leaked"); std::puts("AudioOutTimingTests: all cases passed"); } diff --git a/tests/MemoryTrackerTests.cpp b/tests/MemoryTrackerTests.cpp index a72f60a3..007995ab 100644 --- a/tests/MemoryTrackerTests.cpp +++ b/tests/MemoryTrackerTests.cpp @@ -238,14 +238,31 @@ struct TrackerHarness { MemoryTracker tracker; }; +uint8_t *AllocateFixedGuestRange(uint64_t size, uintptr_t offset) { + // Keep the mapping inside the tracker's guest address space and preserve the + // requested region alignment. A fixed address can be occupied by the host + // process (notably by the macOS runner's ASLR layout). + constexpr uintptr_t first_base = 0x0000000200000000ull; + constexpr uintptr_t stride = 0x0000000100000000ull; + for (uintptr_t attempt = 0; attempt < 256; attempt++) { + auto *wanted = reinterpret_cast(first_base + offset + attempt * stride); + auto *memory = static_cast( + VirtualAlloc(wanted, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + if (memory == wanted) { + return memory; + } + if (memory != nullptr) { + Check(VirtualFree(memory, 0, MEM_RELEASE) != 0, + "release unexpected fixed allocation failed"); + } + } + Check(false, "no free fixed guest address found"); + return nullptr; +} + uint8_t *Allocate(PageManager &manager, uint64_t pages) { - constexpr uintptr_t base = 0x0000000200010000ull; const auto size = manager.GetPageSize() * pages; - auto *memory = static_cast( - VirtualAlloc(reinterpret_cast(base), size, - MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); - Check(memory == reinterpret_cast(base), "fixed VirtualAlloc failed"); - return memory; + return AllocateFixedGuestRange(size, 0x10000); } void Release(uint8_t *memory) { @@ -386,16 +403,10 @@ void TestCpuDirtyUpload() { } void TestRangeInvalidation() { - constexpr uintptr_t base = 0x0000000201000000ull; TrackerHarness harness; auto &tracker = harness.tracker; - auto &page_manager = harness.page_manager; constexpr uint64_t size = Libs::Graphics::TRACKER_REGION_SIZE * 2; - auto *memory = static_cast( - VirtualAlloc(reinterpret_cast(base), size, - MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); - Check(memory == reinterpret_cast(base), - "range invalidation allocation failed"); + auto *memory = AllocateFixedGuestRange(size, 0x1000000); const auto address = reinterpret_cast(memory); tracker.ForEachUploadRange( @@ -614,16 +625,12 @@ void TestGpuDownloadProtectionMirrors() { } void TestCrossRegionUpload() { - constexpr uintptr_t base = 0x0000000200010000ull; constexpr uint64_t region_size = 4ull * 1024ull * 1024ull; TrackerHarness harness; auto &tracker = harness.tracker; auto &page_manager = harness.page_manager; const auto page_size = page_manager.GetPageSize(); - auto *memory = static_cast( - VirtualAlloc(reinterpret_cast(base), region_size * 2, - MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); - Check(memory == reinterpret_cast(base), "fixed VirtualAlloc failed"); + auto *memory = AllocateFixedGuestRange(region_size * 2, 0x10000); const auto address = reinterpret_cast(memory); const auto boundary = (address + region_size - 1) & ~(region_size - 1); uint32_t ranges = 0; diff --git a/tests/PadHapticsTests.cpp b/tests/PadHapticsTests.cpp index 33c7e5c2..d40060b6 100644 --- a/tests/PadHapticsTests.cpp +++ b/tests/PadHapticsTests.cpp @@ -1,11 +1,17 @@ // Exercise the production module with real SDL streams and fake USB endpoints. #include +#include #include #include +#include +#include #include #include #include #include +#include +#include +#include #include namespace { @@ -24,23 +30,32 @@ 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 {}; -std::array pad_rumble {}; -std::array, 4> effects; -std::vector connected_pads {1}; -std::array cached_rumble {}; -bool fail_effect = false, wireless = false; -SDL_AudioStream* default_stream = nullptr; -bool fail_default_resume = false; -int active_controller = 1; +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 {}; +std::array pad_rumble {}; +std::array, 4> effects; +std::vector connected_pads {1}; +std::array cached_rumble {}; +bool fail_effect = false, wireless = false, wireless_pad3 = false; +bool hid_available = false, fail_hid_write = false; +bool duplicate_hid = false; +int hid_opens = 0; +int opened_hid_pad = 0; +std::vector> hid_reports; +std::atomic_bool block_opus_encode {false}, block_hid_write {false}; +std::atomic_int hid_closes {0}; +std::binary_semaphore encode_started {0}, allow_encode {0}; +std::binary_semaphore write_started {0}, allow_write {0}; +SDL_AudioStream* default_stream = nullptr; +bool fail_default_resume = false; +int active_controller = 1; } // namespace namespace Fake { @@ -116,8 +131,9 @@ void DestroyAudioStream(SDL_AudioStream* stream) { return; } const auto it = std::find(streams.begin(), streams.end(), stream); - Check(it != streams.end(), "stream destroyed twice"); - streams.erase(it); + if (it != streams.end()) { + streams.erase(it); + } if (stream == default_stream) { default_stream = nullptr; } @@ -134,6 +150,73 @@ SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) { return id == 1 || id == 3 ? reinterpret_cast(static_cast(id)) : nullptr; } +SDL_GamepadType GetGamepadType(SDL_Gamepad* pad) { + return GetGamepadTypeForID(static_cast(reinterpret_cast(pad))); +} +Uint16 GetGamepadVendor(SDL_Gamepad*) { + return 0x054c; +} +Uint16 GetGamepadProduct(SDL_Gamepad*) { + return 0x0ce6; +} +const char* GetGamepadSerial(SDL_Gamepad* pad) { + return reinterpret_cast(pad) == 3 ? "aa-bb-cc-dd-ee-03" : "aa-bb-cc-dd-ee-01"; +} +Uint64 GetTicksNS() { + return now * 1000000; +} +SDL_Thread* CreateThread(SDL_ThreadFunction, const char*, void*) { + return reinterpret_cast(1); +} +void WaitThread(SDL_Thread*, int*) {} +SDL_hid_device_info* HidEnumerate(unsigned short, unsigned short) { + if (!hid_available) { + return nullptr; + } + static wchar_t serial1[] = L"AA:BB:CC:DD:EE:01"; + static wchar_t serial3[] = L"AA:BB:CC:DD:EE:03"; + static char path1[] = "pad-1"; + static char path3[] = "pad-3"; + static SDL_hid_device_info pad3 {}, pad1 {}; + pad3.path = path3; + pad3.serial_number = duplicate_hid ? serial1 : serial3; + pad3.bus_type = SDL_HID_API_BUS_BLUETOOTH; + pad1.path = path1; + pad1.serial_number = serial1; + pad1.bus_type = SDL_HID_API_BUS_BLUETOOTH; + pad1.next = &pad3; + return &pad1; +} +void HidFreeEnumeration(SDL_hid_device_info*) {} +SDL_hid_device* HidOpenPath(const char* path) { + hid_opens++; + opened_hid_pad = path[4] == '1' ? 1 : 3; + return reinterpret_cast(static_cast(opened_hid_pad)); +} +void HidClose(SDL_hid_device*) { + hid_closes++; +} +int HidWrite(SDL_hid_device*, const unsigned char* data, size_t size) { + if (block_hid_write.exchange(false)) { + write_started.release(); + allow_write.acquire(); + } + if (fail_hid_write || size != 547) { + return -1; + } + std::array report {}; + std::copy_n(data, report.size(), report.begin()); + hid_reports.push_back(report); + return static_cast(size); +} +opus_int32 OpusEncodeFloat(OpusEncoder* encoder, const float* pcm, int frames, + unsigned char* packet, opus_int32 bytes) { + if (block_opus_encode.exchange(false)) { + encode_started.release(); + allow_encode.acquire(); + } + return ::opus_encode_float(encoder, pcm, frames, packet, bytes); +} SDL_JoystickID* GetGamepads(int* count) { *count = static_cast(connected_pads.size()); auto* ids = static_cast( @@ -142,8 +225,10 @@ SDL_JoystickID* GetGamepads(int* count) { ids[connected_pads.size()] = 0; return ids; } -SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad*) { - return wireless ? SDL_JOYSTICK_CONNECTION_WIRELESS : SDL_JOYSTICK_CONNECTION_WIRED; +SDL_JoystickConnectionState GetGamepadConnectionState(SDL_Gamepad* pad) { + return wireless || (wireless_pad3 && reinterpret_cast(pad) == 3) + ? SDL_JOYSTICK_CONNECTION_WIRELESS + : SDL_JOYSTICK_CONNECTION_WIRED; } bool RumbleGamepad(SDL_Gamepad* pad, Uint16 large, Uint16 small, Uint32 duration) { const auto id = reinterpret_cast(pad); @@ -173,22 +258,37 @@ bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) { } } // 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_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_GetGamepadType Fake::GetGamepadType +#define SDL_GetGamepadVendor Fake::GetGamepadVendor +#define SDL_GetGamepadProduct Fake::GetGamepadProduct +#define SDL_GetGamepadSerial Fake::GetGamepadSerial +#define SDL_GetTicksNS Fake::GetTicksNS +#undef SDL_CreateThread +#define SDL_CreateThread Fake::CreateThread +#define SDL_WaitThread Fake::WaitThread +#define SDL_hid_enumerate Fake::HidEnumerate +#define SDL_hid_free_enumeration Fake::HidFreeEnumeration +#define SDL_hid_open_path Fake::HidOpenPath +#define SDL_hid_close Fake::HidClose +#define SDL_hid_write Fake::HidWrite +#define opus_encode_float Fake::OpusEncodeFloat #define SDL_GetGamepadFromID Fake::GetGamepadFromID #define SDL_RumbleGamepad Fake::RumbleGamepad #define SDL_SendGamepadEffect Fake::SendGamepadEffect #define SDL_GetGamepads Fake::GetGamepads #define SDL_GetGamepadConnectionState Fake::GetGamepadConnectionState +#include "libs/dualSenseBluetooth.cpp" #include "libs/dualSenseHaptics.cpp" #include "libs/audio.cpp" #undef SDL_GetTicks @@ -202,6 +302,19 @@ bool SendGamepadEffect(SDL_Gamepad* pad, const void* data, int size) { #undef SDL_DestroyAudioStream #undef SDL_GetAudioStreamDevice #undef SDL_GetGamepadTypeForID +#undef SDL_GetGamepadType +#undef SDL_GetGamepadVendor +#undef SDL_GetGamepadProduct +#undef SDL_GetGamepadSerial +#undef SDL_GetTicksNS +#undef SDL_CreateThread +#undef SDL_WaitThread +#undef SDL_hid_enumerate +#undef SDL_hid_free_enumeration +#undef SDL_hid_open_path +#undef SDL_hid_close +#undef SDL_hid_write +#undef opus_encode_float #undef SDL_GetGamepadFromID #undef SDL_RumbleGamepad #undef SDL_SendGamepadEffect @@ -228,6 +341,7 @@ double GetTimeMs() { namespace { namespace Haptics = Libs::Controller::DualSenseHaptics; +namespace Bluetooth = Libs::Controller::DualSenseBluetooth; using Port = std::unique_ptr; constexpr std::array unity {32768, 32768}; constexpr std::array pcm {0.5f, 0.5f, 0.5f, 0.5f}; @@ -240,11 +354,17 @@ struct Fixture { cached_rumble = {}; effects = {}; connected_pads = {1}; - fail_effect = wireless = false; - default_stream = nullptr; + fail_effect = wireless = wireless_pad3 = false; + hid_available = fail_hid_write = false; + duplicate_hid = false; + hid_opens = 0; + hid_closes = 0; + opened_hid_pad = 0; + hid_reports.clear(); + default_stream = nullptr; fail_default_resume = false; - active_controller = 1; - now = 1000; + active_controller = 1; + now = 1000; opens = rumble_calls = 0; fail_open = fail_resume = false; actual_channels = 4; @@ -255,8 +375,8 @@ struct Fixture { } }; -Port Open(bool speaker = false) { - auto* stream = Haptics::Open(48000, speaker); +Port Open(bool speaker = false, uint32_t freq = 48000) { + auto* stream = Haptics::Open(freq, speaker); Check(stream != nullptr, "port open failed"); return Port(stream, Haptics::Close); } @@ -516,6 +636,446 @@ void TestSpeakerRequiresUnambiguousUsbDevice() { "USB to Bluetooth change retained the USB speaker"); } +void TestBluetoothAudioAndIdentity() { + Fixture f; + wireless = hid_available = true; + connected_pads = {1, 3}; + devices.clear(); // Bluetooth does not enumerate a quad USB audio endpoint. + auto vibration = Open(); + auto speaker = Open(true); + std::vector haptic(2048, 0.5f); + std::vector sound(2048, 0.25f); + Check(Haptics::SetVibration(1, 100, 80) && pad_rumble[1].large == 100 * 0x101, + "Bluetooth controller did not receive normal rumble"); + Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, unity.data()) != 0, + "Bluetooth haptics did not open"); + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0, + "Bluetooth speaker did not open"); + Check(hid_opens == 1 && opened_hid_pad == 1 && effects[1][7] == 0x30, + "Bluetooth audio was not matched to the active controller"); + Check(pad_rumble[1].large == 0, "Bluetooth haptics did not suppress motor rumble"); + auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000); + Check(pending.size() == 1, "Bluetooth streams were not combined in one report"); + Libs::Controller::DualSenseBluetooth::Send(pending); + Check(hid_reports.size() == 1, "Bluetooth audio report was not sent"); + const auto& report = hid_reports.back(); + Check(report[0] == 0x39 && report[2] == 0x91 && report[3] == 6 && report[4] == 0x7e && + report[10] == 0xd2 && report[11] == 64 && report[140] == 0xd3 && report[141] == 200, + "Bluetooth audio sub-packets are malformed"); + Check(report[12] > 0 && report[13] > 0, + "haptic waveform was not downsampled into the actuator channels"); + uint8_t prefix = 0xa2; + auto crc = SDL_crc32(0, &prefix, 1); + crc = SDL_crc32(crc, report.data(), report.size() - 4); + for (size_t i = 0; i < 4; i++) { + Check(report[report.size() - 4 + i] == static_cast(crc >> (8 * i)), + "Bluetooth audio CRC is invalid"); + } + int opus_error = OPUS_OK; + auto* decoder = opus_decoder_create(48000, 2, &opus_error); + Check(decoder != nullptr && opus_error == OPUS_OK, "Opus decoder unavailable in test"); + std::array decoded {}; + Check(opus_decode_float(decoder, report.data() + 142, 200, decoded.data(), 480, 0) == 480, + "Bluetooth speaker packet could not be decoded"); + opus_decoder_destroy(decoder); + const auto mean = std::accumulate(decoded.begin(), decoded.end(), 0.0f) / decoded.size(); + Check(mean > 0.05f && mean < 0.35f, "Bluetooth speaker audio was not preserved"); + Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(), + "Bluetooth sender ignored report pacing"); + now += 300; + Haptics::UpdateBluetoothRumble(); + Check(pad_rumble[1].large == 100 * 0x101, + "Bluetooth haptics did not restore normal rumble afterward"); + + // A different active DualSense has a different HID address, even if both are + // paired. + Check(Haptics::Queue(vibration.get(), 3, haptic.data(), 1024, 2, true, unity.data()) != 0 && + opened_hid_pad == 3, + "Bluetooth stream followed the wrong controller"); + Check(Haptics::Queue(speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != 0, + "Bluetooth speaker did not follow the selected controller"); + Check(effects[1][7] == 0, "old Bluetooth controller remained speaker-routed"); + wireless = false; + connected_pads = {3}; + devices = {{20, "Speakers (DualSense Wireless Controller)", 4}}; + Check(Haptics::Queue(speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != 0 && + opened_device == 20 && effects[3][7] == 0x30, + "Bluetooth to USB change did not reopen the USB speaker immediately"); + speaker.reset(); + vibration.reset(); +} + +void TestBluetoothFailureFallsBack() { + Fixture f; + wireless = hid_available = true; + auto speaker = Open(true); + std::vector sound(2048, 0.25f); + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0, + "Bluetooth port did not initially open"); + fail_hid_write = true; + auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000); + Libs::Controller::DualSenseBluetooth::Send(pending); + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) == 0 && + effects[1][7] == 0, + "failed HID write did not restore the normal audio fallback"); + speaker.reset(); +} + +void TestBluetoothFormatsAndRoutingFailure() { + Fixture f; + wireless = hid_available = fail_effect = true; + auto speaker = Open(true); + Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 && + !Haptics::UsesBluetooth(speaker.get()) && hid_closes == 1 && effects[1].empty(), + "failed Bluetooth speaker routing leaked a session or suppressed fallback"); + fail_effect = false; + now += 2001; + std::vector mono(1024, 16384); + const int volume = 16384; + Queue(speaker, mono.data(), 1024, 1, false, &volume); + auto pending = Bluetooth::Prepare(now * 1000000); + Check(pending.size() == 1 && hid_opens == 2 && effects[1][7] == 0x30 && + std::abs(pending[0].audio[200] - 0.25f) < 1e-4f && + std::abs(pending[0].audio[201] - 0.25f) < 1e-4f, + "Bluetooth mono int16 volume mapping or speaker routing recovery failed"); +} + +void TestBluetoothEncodingDoesNotBlockAudioQueue() { + Fixture f; + wireless = hid_available = true; + auto speaker = Open(true); + std::vector sound(2048, 0.25f); + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0, + "Bluetooth speaker did not open for concurrent encoding test"); + block_opus_encode = true; + std::thread encoder([&] { + auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000); + Libs::Controller::DualSenseBluetooth::Send(pending); + }); + encode_started.acquire(); + std::binary_semaphore queue_finished {0}; + std::atomic_bool queue_ok {false}; + std::thread audio([&] { + queue_ok = Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, + unity.data()) != 0; + queue_finished.release(); + }); + const bool queued_without_encoder = + queue_finished.try_acquire_for(std::chrono::seconds(2)); + allow_encode.release(); + audio.join(); + encoder.join(); + Check(queued_without_encoder && queue_ok, + "Bluetooth audio queue waited for Opus encoding"); + speaker.reset(); +} + +void TestBluetoothCloseCancelsPreparedReport() { + Fixture f; + wireless = hid_available = true; + auto speaker = Open(true); + auto vibration = Open(); + std::vector sound(2048, 0.25f); + Queue(speaker, sound.data(), 1024); + Queue(vibration, sound.data(), 1024); + const auto base = now * 1000000; + auto pending = Bluetooth::Prepare(base); + Check(pending.size() == 1, "Bluetooth report was not prepared before stream close"); + speaker.reset(); + Bluetooth::Send(pending); + Check(hid_reports.empty(), "closed speaker's prepared report was sent"); + Queue(vibration, sound.data(), 1024); + Bluetooth::Send(Bluetooth::Prepare(base + Bluetooth::PERIOD_NS)); + Check(hid_reports.size() == 1 && hid_reports.back()[140] == 0, + "closing the speaker prevented the remaining haptic stream from sending"); +} + +void TestBluetoothCloseDuringEncoding() { + Fixture f; + wireless = hid_available = true; + auto speaker = Open(true); + std::vector sound(2048, 0.25f); + Queue(speaker, sound.data(), 1024); + block_opus_encode = true; + std::thread encoder([&] { Bluetooth::Send(Bluetooth::Prepare(now * 1000000)); }); + encode_started.acquire(); + std::binary_semaphore close_finished {0}; + std::thread closer([&] { + speaker.reset(); + close_finished.release(); + }); + const bool closed_without_encoder = close_finished.try_acquire_for(std::chrono::seconds(2)); + const bool hid_alive_during_encode = hid_closes == 0; + allow_encode.release(); + closer.join(); + encoder.join(); + Check(closed_without_encoder && hid_alive_during_encode && hid_closes == 1 && + hid_reports.empty(), + "closing during encoding blocked, destroyed a live session, or sent stale audio"); +} + +void TestBluetoothCloseWaitsForWriteWithoutBlockingQueue() { + Fixture f; + wireless = hid_available = true; + auto speaker = Open(true); + auto vibration = Open(); + std::vector sound(2048, 0.25f); + Queue(speaker, sound.data(), 1024); + Queue(vibration, sound.data(), 1024); + auto pending = Bluetooth::Prepare(now * 1000000); + Check(pending.size() == 1, "Bluetooth report was not prepared before blocked write"); + block_hid_write = true; + std::thread sender([&] { Bluetooth::Send(pending); }); + write_started.acquire(); + std::binary_semaphore close_started {0}, close_finished {0}, queue_finished {0}; + std::thread closer([&] { + close_started.release(); + speaker.reset(); + close_finished.release(); + }); + close_started.acquire(); + const bool closed_before_write = close_finished.try_acquire_for(std::chrono::milliseconds(50)); + std::atomic_bool queue_ok {false}; + std::thread audio([&] { + queue_ok = + Haptics::Queue(vibration.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0; + queue_finished.release(); + }); + const bool queued_without_write = queue_finished.try_acquire_for(std::chrono::seconds(2)); + allow_write.release(); + audio.join(); + closer.join(); + sender.join(); + Check(!closed_before_write && queued_without_write && queue_ok && hid_reports.size() == 1 && + effects[1][7] == 0, + "close failed to drain the HID write independently of the audio queue"); +} + +void TestBluetoothSpeakerWaitsForItsAudioBlock() { + Fixture f; + wireless = hid_available = true; + auto vibration = Open(); + auto speaker = Open(true); + std::vector haptic(2048, 0.5f); + std::vector sound(2048, 0.25f); + Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, unity.data()) != 0, + "Bluetooth vibration stream did not open"); + Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(), + "haptics sent a silent speaker packet before the audio batch arrived"); + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 256, 2, true, unity.data()) != 0, + "Bluetooth speaker did not accept a short block"); + Check(Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000).empty(), + "short Bluetooth speaker block was padded with silence immediately"); + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 768, 2, true, unity.data()) != 0, + "Bluetooth speaker did not accept the remainder of its block"); + auto reports = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000); + Check(reports.size() == 1 && reports[0].audio[100] > 0.2f && reports[0].data[12] > 0, + "Bluetooth speaker and haptics were not combined into a complete report"); + speaker.reset(); + vibration.reset(); +} + +void TestBluetoothShortHapticBlock() { + Fixture f; + wireless = hid_available = true; + auto vibration = Open(); + std::vector sound(2048, 0.25f); + Queue(vibration, sound.data(), 256); + const auto base = now * 1000000; + Check(Bluetooth::Prepare(base).empty() && Bluetooth::Prepare(base + 1000000).empty(), + "short haptic block was padded with silence before its remainder arrived"); + Queue(vibration, sound.data(), 768); + Check(Bluetooth::Prepare(base + 1000000).empty(), + "completed haptic block skipped its speaker coalescing window"); + auto pending = Bluetooth::Prepare(base + 2000000); + Check(pending.size() == 1 && pending[0].data[12 + 100] > 0, + "complete haptic block did not play after its remainder arrived"); +} + +void TestBluetoothIncrementalBatchCoalesces() { + Fixture f; + wireless = hid_available = true; + auto vibration = Open(); + auto speaker = Open(true); + std::array sound; + sound.fill(0.25f); + for (int block = 0; block < 3; ++block) { + Queue(vibration, sound.data(), 256); + Queue(speaker, sound.data(), 256); + Check(Bluetooth::Prepare(now * 1000000).empty(), + "incremental Bluetooth batch was padded before it was complete"); + now += 5; + } + Queue(vibration, sound.data(), 256); + Check(Bluetooth::Prepare(now * 1000000).empty(), + "completed haptics sent before the same batch's final speaker block arrived"); + Queue(speaker, sound.data(), 256); + auto pending = Bluetooth::Prepare(now * 1000000); + Check(pending.size() == 1 && pending[0].audio[1800] > 0.2f && pending[0].data[12 + 100] > 0, + "completed incremental speaker and haptics batch did not play immediately"); +} + +void TestBluetoothLowRateCompleteBlock() { + Fixture f; + wireless = hid_available = true; + auto speaker = Open(true, 8000); + std::array sound; + sound.fill(0.25f); + Queue(speaker, sound.data(), 171); + auto pending = Bluetooth::Prepare(now * 1000000); + Check(pending.size() == 1 && pending[0].audio[1000] > 0.2f, + "resampler lookahead delayed a complete low-rate speaker block"); +} + +void TestBluetoothCompleteStreamDoesNotWaitForPartialPeer() { + Fixture f; + wireless = hid_available = true; + auto complete = Open(true); + auto partial = Open(true); + std::vector sound(2048, 0.25f); + Queue(complete, sound.data(), 1024); + Queue(partial, sound.data(), 256); + auto pending = Bluetooth::Prepare(now * 1000000); + Check(pending.size() == 1 && pending[0].audio[100] > 0.4f && pending[0].audio[1000] > 0.2f, + "complete speaker stream waited for a partial peer instead of mixing available audio"); +} + +void TestBluetoothShortFinalBlock() { + for (bool speaker: {false, true}) { + Fixture f; + wireless = hid_available = true; + auto port = Open(speaker); + std::vector sound(2048, 0.25f); + Queue(port, sound.data(), 256); + const auto base = now * 1000000; + Check(Bluetooth::Prepare(base).empty(), + "short final Bluetooth block sent before more audio could arrive"); + Check(Bluetooth::Prepare(base + Bluetooth::PERIOD_NS).size() == 1, + "short final Bluetooth block was never played"); + } +} + +void TestBluetoothOverflowKeepsRecentAudio() { + Fixture f; + wireless = hid_available = true; + auto speaker = Open(true); + std::vector sound(2048); + for (int block = 1; block <= 4; ++block) { + std::fill(sound.begin(), sound.end(), block * 0.1f); + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, + unity.data()) != 0, + "Bluetooth speaker overflow queue failed"); + } + auto first = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000); + Check(first.size() == 1 && first[0].audio[200] > 0.15f && first[0].audio[200] < 0.25f, + "Bluetooth queue overflow lost all buffered audio instead of the oldest block"); + auto second = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000 + + Libs::Controller::DualSenseBluetooth::PERIOD_NS); + Check(second.size() == 1 && second[0].audio[200] > 0.25f && second[0].audio[200] < 0.35f, + "Bluetooth queue overflow did not preserve the next audio block"); + speaker.reset(); +} + +void TestBluetoothHapticOverflowKeepsRecentAudio() { + Fixture f; + wireless = hid_available = true; + auto vibration = Open(); + std::vector haptic(2048); + for (int block = 1; block <= 4; ++block) { + std::fill(haptic.begin(), haptic.end(), block * 0.1f); + Check(Haptics::Queue(vibration.get(), 1, haptic.data(), 1024, 2, true, + unity.data()) != 0, + "Bluetooth haptic overflow queue failed"); + } + const auto base = now * 1000000; + Check(Libs::Controller::DualSenseBluetooth::Prepare(base).empty(), + "haptic-only report skipped its coalescing window"); + auto first = Libs::Controller::DualSenseBluetooth::Prepare(base + 1000000); + Check(first.size() == 1 && first[0].data[12 + 32] > 18 && first[0].data[12 + 32] < 32, + "Bluetooth haptic overflow discarded buffered waveform instead of one report"); + vibration.reset(); +} + +void TestBluetoothRecoversAfterLateWrite() { + Fixture f; + wireless = hid_available = true; + auto speaker = Open(true); + std::vector sound(2048, 0.25f); + for (int i = 0; i < 3; ++i) { + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, + unity.data()) != 0, + "Bluetooth speaker could not buffer three reports"); + } + const auto base = now * 1000000; + const auto period = Libs::Controller::DualSenseBluetooth::PERIOD_NS; + Check(Libs::Controller::DualSenseBluetooth::Prepare(base).size() == 1, + "first Bluetooth report was not prepared"); + Check(Libs::Controller::DualSenseBluetooth::Prepare(base + period + 10000000).size() == 1, + "late Bluetooth report was not prepared"); + Check(Libs::Controller::DualSenseBluetooth::Prepare(base + 2 * period).size() == 1, + "Bluetooth sender did not recover its schedule after a late write"); + speaker.reset(); +} + +void TestUsbOverflowKeepsRecentAudio() { + Fixture f; + auto speaker = Open(true); + std::vector sound(2048); + for (int block = 1; block <= 4; ++block) { + std::fill(sound.begin(), sound.end(), block * 0.1f); + Check(Haptics::Queue(speaker.get(), 1, sound.data(), 1024, 2, true, + unity.data()) != 0, + "USB speaker overflow queue failed"); + } + std::array first {}; + Check(!streams.empty() && + SDL_GetAudioStreamData(streams.back(), first.data(), sizeof(first)) == + sizeof(first) && + first[0] > 0.15f && first[0] < 0.25f, + "USB queue overflow did not preserve the newest buffered audio"); + // A single oversized input must also preserve only its newest 80 ms. + sound.assign(4096 * 2, 0.1f); + std::fill(sound.begin() + 256 * 2, sound.end(), 0.5f); + const auto queued = Haptics::Queue(speaker.get(), 1, sound.data(), 4096, 2, true, unity.data()); + Check(queued == 80000 && SDL_GetAudioStreamQueued(streams.back()) == 3840 * 4 * sizeof(float) && + SDL_GetAudioStreamData(streams.back(), first.data(), sizeof(first)) == + sizeof(first) && + first[0] == 0.5f && first[1] == 0.5f, + "oversized USB block did not retain only the newest 80 ms"); + speaker.reset(); +} + +void TestBluetoothAmbiguousDevice() { + Fixture f; + wireless = hid_available = duplicate_hid = true; + auto speaker = Open(true); + Check(Haptics::Queue(speaker.get(), 1, pcm.data(), 2, 2, true, unity.data()) == 0 && + hid_opens == 0 && effects[1].empty(), + "ambiguous Bluetooth devices were routed arbitrarily"); + speaker.reset(); +} + +void TestUsbAndBluetoothFeatureParity() { + Fixture f; + connected_pads = {1, 3}; + wireless_pad3 = hid_available = true; + auto wired_speaker = Open(true); + auto wireless_speaker = Open(true); + std::vector sound(2048, 0.25f); + Check(Haptics::Queue(wired_speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0 && + opened_device == 10, + "Bluetooth pad disabled the wired pad speaker"); + Check(Haptics::Queue(wireless_speaker.get(), 3, sound.data(), 1024, 2, true, unity.data()) != + 0 && + opened_hid_pad == 3, + "wired pad disabled the Bluetooth speaker"); + Check(Haptics::Queue(wired_speaker.get(), 1, sound.data(), 1024, 2, true, unity.data()) != 0 && + effects[1][7] == 0x30 && effects[3][7] == 0, + "returning to the wired speaker left Bluetooth routing active"); + wireless_speaker.reset(); + wired_speaker.reset(); +} + void TestAudioSpeakerFallback() { Fixture f; Libs::Audio::Audio audio; @@ -545,6 +1105,26 @@ void TestAudioSpeakerRouting() { ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0}); } +void TestBluetoothAudioSpeakerRouting() { + Fixture f; + wireless = hid_available = true; + devices.clear(); + Libs::Audio::Audio audio; + const auto port = audio.AudioOutOpen(4, 1024, 48000, Libs::Audio::Audio::Format::FloatStereo); + Check(port.IsValid() && default_stream != nullptr, "Bluetooth pad speaker port did not open"); + std::vector sound(2048, 0.25f); + Libs::Audio::Audio::OutputParam output {port, sound.data()}; + Check(audio.AudioOutOutputs(&output, 1, false) == 1024 && hid_opens == 1 && + SDL_GetAudioStreamQueued(default_stream) == 0, + "Bluetooth pad speaker also played on the host output"); + fail_hid_write = true; + auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000); + Libs::Controller::DualSenseBluetooth::Send(pending); + Check(audio.AudioOutOutputs(&output, 1, false) == 1024 && + SDL_GetAudioStreamQueued(default_stream) > 0, + "Bluetooth write failure did not use the normal speaker output"); +} + void TestAudioDefaultResumeFailure() { Fixture f; Libs::Audio::Audio audio; @@ -564,6 +1144,7 @@ int main() { SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy"); TestAudioSpeakerFallback(); TestAudioSpeakerRouting(); + TestBluetoothAudioSpeakerRouting(); TestAudioDefaultResumeFailure(); TestFormatsAndVolume(); TestDiscoveryAndHotplug(); @@ -571,6 +1152,25 @@ int main() { TestSpeakerUnplug(); TestSpeakerSwitchAndFailures(); TestSpeakerRequiresUnambiguousUsbDevice(); + TestBluetoothAudioAndIdentity(); + TestBluetoothFailureFallsBack(); + TestBluetoothFormatsAndRoutingFailure(); + TestBluetoothEncodingDoesNotBlockAudioQueue(); + TestBluetoothCloseCancelsPreparedReport(); + TestBluetoothCloseDuringEncoding(); + TestBluetoothCloseWaitsForWriteWithoutBlockingQueue(); + TestBluetoothSpeakerWaitsForItsAudioBlock(); + TestBluetoothShortHapticBlock(); + TestBluetoothIncrementalBatchCoalesces(); + TestBluetoothLowRateCompleteBlock(); + TestBluetoothCompleteStreamDoesNotWaitForPartialPeer(); + TestBluetoothShortFinalBlock(); + TestBluetoothOverflowKeepsRecentAudio(); + TestBluetoothHapticOverflowKeepsRecentAudio(); + TestBluetoothRecoversAfterLateWrite(); + TestUsbOverflowKeepsRecentAudio(); + TestBluetoothAmbiguousDevice(); + TestUsbAndBluetoothFeatureParity(); TestFailuresAndBoundedQueue(); TestRumbleLeaseAndDuration(); TestSwitchStopsOldRumble(); diff --git a/tests/PageManagerTests.cpp b/tests/PageManagerTests.cpp index d1a5e631..effb8c95 100644 --- a/tests/PageManagerTests.cpp +++ b/tests/PageManagerTests.cpp @@ -192,13 +192,25 @@ uint8_t *Allocate(uint64_t size, uint32_t protection = PAGE_READWRITE, Check(memory == reinterpret_cast(test_address), "fixed low VirtualAlloc failed"); #elif defined(__APPLE__) - mach_vm_address_t raw = test_address; - Check(mach_vm_allocate(mach_task_self(), &raw, size, VM_FLAGS_FIXED) == - KERN_SUCCESS && - mach_vm_protect(mach_task_self(), raw, size, false, - static_cast(ToHostProt(protection))) == - KERN_SUCCESS, - "fixed low mach_vm_allocate failed"); + mach_vm_address_t raw = 0; + bool allocated = false; + constexpr uintptr_t stride = 0x0000000100000000ull; + for (uintptr_t candidate = test_address; + Libs::Graphics::GuestRange{candidate, size}.Valid(); candidate += stride) { + raw = candidate; + if (mach_vm_allocate(mach_task_self(), &raw, size, VM_FLAGS_FIXED) != + KERN_SUCCESS) { + continue; + } + if (mach_vm_protect(mach_task_self(), raw, size, false, + static_cast(ToHostProt(protection))) == + KERN_SUCCESS) { + allocated = true; + break; + } + mach_vm_deallocate(mach_task_self(), raw, size); + } + Check(allocated, "no free fixed guest address found"); auto *memory = reinterpret_cast(raw); AllocationSizes()[memory] = static_cast(size); #else