diff --git a/CMakeLists.txt b/CMakeLists.txt index e1bb24806..d22f39a5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -227,7 +227,7 @@ else() add_executable(${PROJECT_NAME} core/emulator.cpp) endif() -set_target_properties(${PROJECT_NAME} PROPERTIES +set_target_properties(${PROJECT_NAME} PROPERTIES CMAKE_CXX_STANDARD 20 CMAKE_CXX_STANDARD_REQUIRED ON) @@ -481,6 +481,10 @@ if(NOT LIBRETRO) core/sdl/sdl_gamepad.h core/sdl/sdl_keyboard.h core/sdl/sdl_keyboard_mac.h + core/sdl/dreamlink.cpp + core/sdl/dreamlink.h + core/sdl/dreampicoport.cpp + core/sdl/dreampicoport.h core/sdl/dreamconn.cpp core/sdl/dreamconn.h) @@ -523,7 +527,7 @@ if(NOT WITH_SYSTEM_ZLIB) # help libzip find the package set(ZLIB_FOUND TRUE) set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/${ZLIB_RELATIVE_PATH}" "${CMAKE_CURRENT_BINARY_DIR}/${ZLIB_RELATIVE_PATH}") - + cmake_policy(SET CMP0026 OLD) get_target_property(ZLIB_LIBRARY_RELEASE zlibstatic LOCATION) get_target_property(ZLIB_LIBRARY_DEBUG zlibstatic LOCATION_Debug) diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index d59f666f4..b1b7de7d2 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -203,6 +203,7 @@ Option PerGameVmu("PerGameVmu", false, "config"); #ifdef _WIN32 Option UseRawInput("RawInput", false, "input"); #endif +Option UsePhysicalVmuMemory("UsePhysicalVmuMemory", false); #ifdef USE_LUA Option LuaFileName("LuaFileName", "flycast.lua"); diff --git a/core/cfg/option.h b/core/cfg/option.h index 81703e728..9e8d8e645 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -543,6 +543,7 @@ extern Option UseRawInput; #else constexpr bool UseRawInput = false; #endif +extern Option UsePhysicalVmuMemory; #ifdef USE_LUA extern Option LuaFileName; diff --git a/core/hw/maple/maple_devs.cpp b/core/hw/maple/maple_devs.cpp index 988cb6adb..ac2a6414a 100755 --- a/core/hw/maple/maple_devs.cpp +++ b/core/hw/maple/maple_devs.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include const char* maple_sega_controller_name = "Dreamcast Controller"; const char* maple_sega_vmu_name = "Visual Memory"; @@ -369,7 +371,7 @@ struct maple_sega_vmu: maple_base fullSaveNeeded = true; } - bool fullSave() + virtual bool fullSave() { if (file == nullptr) return false; @@ -2107,29 +2109,215 @@ std::shared_ptr maple_Create(MapleDeviceType type) } #if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) && defined(USE_SDL) && !defined(LIBRETRO) -#include "sdl/dreamconn.h" +#include "sdl/dreamlink.h" #include #include -struct DreamConnVmu : public maple_sega_vmu +struct DreamLinkVmu : public maple_sega_vmu { - std::shared_ptr dreamconn; + bool running = true; - DreamConnVmu(std::shared_ptr dreamconn) : dreamconn(dreamconn) { + std::shared_ptr dreamlink; + bool useRealVmuMemory; //!< Set this to true to use physical VMU memory, false for virtual memory + std::chrono::time_point lastWriteTime; + bool mirroredBlocks[256]; //!< Set to true for block that has been loaded/written + s16 lastWriteBlock = -1; + + std::list blocksToWrite; + std::mutex writeMutex; + std::condition_variable writeCv; + std::thread writeThread; + + static u64 lastNotifyTime; + static u64 lastErrorNotifyTime; + + DreamLinkVmu(std::shared_ptr dreamlink) : + dreamlink(dreamlink), + writeThread([this](){writeEntrypoint();}) + { + // Initialize useRealVmuMemory with our config setting + useRealVmuMemory = config::UsePhysicalVmuMemory; + } + + virtual ~DreamLinkVmu() { + running = false; + + // Entering lock context + { + std::unique_lock lock(writeMutex); + writeCv.notify_all(); + } + + writeThread.join(); + } + + void OnSetup() override + { + // Update useRealVmuMemory in case config changed + useRealVmuMemory = config::UsePhysicalVmuMemory; + + // All data must be re-read + memset(mirroredBlocks, 0, sizeof(mirroredBlocks)); + + if (useRealVmuMemory) + { + // Ensure file is not being used + if (file != nullptr) + { + std::fclose(file); + file = nullptr; + } + + memset(flash_data, 0, sizeof(flash_data)); + memset(lcd_data, 0, sizeof(lcd_data)); + + } + else + { + maple_sega_vmu::OnSetup(); + } + } + + bool fullSave() override + { + if (useRealVmuMemory) + { + // Skip virtual save when using physical VMU + //DEBUG_LOG(MAPLE, "Not saving because this is a real vmu"); + NOTICE_LOG(MAPLE, "Saving to physical VMU"); + + return true; + } + else + { + return maple_sega_vmu::fullSave(); + } } u32 dma(u32 cmd) override { + // Physical VMU logic if (dma_count_in >= 4) { - const u32 functionId = *(u32 *)dma_buffer_in; - if ((cmd == MDCF_BlockWrite && functionId == MFID_2_LCD) // LCD screen - || (cmd == MDCF_SetCondition && functionId == MFID_3_Clock)) // Buzzer + const u32 functionId = *(u32*)dma_buffer_in; + const MapleMsg* msg = reinterpret_cast(dma_buffer_in - 4); + + if (functionId == MFID_1_Storage) { - const MapleMsg *msg = reinterpret_cast(dma_buffer_in - 4); - dreamconn->send(*msg); + if (useRealVmuMemory) + { + const u64 currentTime = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + + // Only show notification once every 6 seconds to avoid spam + if (cmd == MDCF_BlockRead && + (currentTime - lastNotifyTime) > 4000 && + (currentTime - lastErrorNotifyTime) > 4000) + { + // This is a read operation (loading) + os_notify("ATTENTION: Loading from a physical VMU", 6000, + "Game data is being loaded from your physical VMU"); + lastNotifyTime = currentTime; + } + + switch (cmd) + { + case MDCF_BlockWrite: + { + // Throw away function + r32(); + + // Save the write to RAM + u32 bph=r32(); + u32 Block = lastWriteBlock = (SWAP32(bph))&0xffff; + u32 Phase = ((SWAP32(bph))>>16)&0xff; + u32 write_adr=Block*512+Phase*(512/4); + u32 write_len=r_count(); + DEBUG_LOG(MAPLE, "VMU %s block write: Block %d Phase %d addr %x len %d", logical_port, Block, Phase, write_adr, write_len); + if (write_adr + write_len > sizeof(flash_data)) + { + INFO_LOG(MAPLE, "Failed to write VMU %s: overflow", logical_port); + skip(write_len); + return MDRE_FileError; //invalid params + } + rptr(&flash_data[write_adr],write_len); + + // All done - wait until GetLastError to queue the write + return MDRS_DeviceReply; + } + + case MDCF_GetLastError: + { + mirroredBlocks[lastWriteBlock] = true; + + // Entering lock context + { + std::unique_lock lock(writeMutex); + if (std::find(blocksToWrite.begin(), blocksToWrite.end(), lastWriteBlock) == blocksToWrite.end()) + { + blocksToWrite.push_back(lastWriteBlock); + writeCv.notify_all(); + } + } + + lastWriteTime = std::chrono::system_clock::now(); + return MDRS_DeviceReply; + } + + case MDCF_BlockRead: + { + u8 requestBlock = msg->data[7]; + if (!mirroredBlocks[requestBlock]) { + // Try up to 4 times to read + bool readSuccess = false; + for (u32 i = 0; i < 4; ++i) { + if (i > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + } + + MapleMsg rcvMsg; + if (dreamlink->send(*msg, rcvMsg) && rcvMsg.size == 130) { + // Something read! + u8 block = rcvMsg.data[7]; + memcpy(&flash_data[block * 4 * 128], &rcvMsg.data[8], 4 * 128); + mirroredBlocks[block] = true; + readSuccess = true; + break; + } + } + + if (!readSuccess) { + ERROR_LOG(MAPLE, "Failed to read VMU %s: I/O error", logical_port); + return MDRE_FileError; // I/O error + } + } + + break; + } + + default: + // do nothing + break; + } + } + } + else if (functionId == MFID_2_LCD) + { + if (cmd == MDCF_BlockWrite) + { + dreamlink->send(*msg); + } + } + else if (functionId == MFID_3_Clock) + { + if (cmd == MDCF_SetCondition) + { + dreamlink->send(*msg); + } } } + + // If made it here, call base's dma to handle return value return maple_sega_vmu::dma(cmd); } @@ -2143,10 +2331,14 @@ struct DreamConnVmu : public maple_sega_vmu void copyOut(std::shared_ptr other) { - memcpy(other->flash_data, flash_data, sizeof(other->flash_data)); - memcpy(other->lcd_data, lcd_data, sizeof(other->lcd_data)); - memcpy(other->lcd_data_decoded, lcd_data_decoded, sizeof(other->lcd_data_decoded)); - other->fullSaveNeeded = fullSaveNeeded; + // Never copy data to virtual VMU if physical VMU is enabled + if (!config::UsePhysicalVmuMemory && !useRealVmuMemory) + { + memcpy(other->flash_data, flash_data, sizeof(other->flash_data)); + memcpy(other->lcd_data, lcd_data, sizeof(other->lcd_data)); + memcpy(other->lcd_data_decoded, lcd_data_decoded, sizeof(other->lcd_data_decoded)); + other->fullSaveNeeded = fullSaveNeeded; + } } void updateScreen() @@ -2156,130 +2348,301 @@ struct DreamConnVmu : public maple_sega_vmu msg.destAP = maple_port; msg.originAP = bus_id << 6; msg.size = 2 + sizeof(lcd_data) / 4; - *(u32 *)&msg.data[0] = MFID_2_LCD; - *(u32 *)&msg.data[4] = 0; // PT, phase, block# + *(u32*)&msg.data[0] = MFID_2_LCD; + *(u32*)&msg.data[4] = 0; // PT, phase, block# memcpy(&msg.data[8], lcd_data, sizeof(lcd_data)); - dreamconn->send(msg); + dreamlink->send(msg); + } + +private: + //! Thread entrypoint for write operations + void writeEntrypoint() + { + while (true) + { + u8 block = 0; + + // Entering lock context + { + std::unique_lock lock(writeMutex); + writeCv.wait(lock, [this](){ return (!running || !blocksToWrite.empty()); }); + + if (!running) + { + break; + } + + block = blocksToWrite.front(); + blocksToWrite.pop_front(); + } + + const u64 currentTime = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + + // Only show notification once every 6 seconds to avoid spam + if ((currentTime - lastNotifyTime) > 4000 && (currentTime - lastErrorNotifyTime) > 4000) + { + // This is a write operation (saving) + os_notify("ATTENTION: You are saving to a physical VMU", 6000, + "Do not disconnect the VMU or close the game"); + lastNotifyTime = currentTime; + } + + bool writeSuccess = true; + const u8* blockData = &flash_data[block * 4 * 128]; + std::chrono::milliseconds delay(10); + const std::chrono::milliseconds delayInc(5); + // Try up to 4 times to write + for (u32 i = 0; i < 4; ++i) { + if (i > 0) { + // Slow down writes to avoid overloading the VMU + delay += delayInc; + } + + writeSuccess = true; + + // 4 write phases per block + for (u32 phase = 0; phase < 4; ++phase) { + MapleMsg writeMsg; + writeMsg.command = MDCF_BlockWrite; + writeMsg.destAP = (bus_id << 6) | (1 << bus_port);; + writeMsg.originAP = (bus_id << 6); + writeMsg.size = 34; + writeMsg.setWord(MFID_1_Storage, 0); + const u32 locationWord = (block << 24) | (phase << 8); + writeMsg.setWord(locationWord, 1); + memcpy(&writeMsg.data[8], &blockData[phase * 128], 128); + + // Delay before writing + std::this_thread::sleep_for(delay); + + MapleMsg rcvMsg; + if (!dreamlink->send(writeMsg, rcvMsg) || rcvMsg.command != MDRS_DeviceReply) { + // Not acknowledged + writeSuccess = false; + break; + } + } + + if (writeSuccess) { + // Delay before committing + std::this_thread::sleep_for(delay); + + // Send the GetLastError command to commit the data + MapleMsg writeMsg; + writeMsg.command = MDCF_GetLastError; + writeMsg.destAP = (bus_id << 6) | (1 << bus_port);; + writeMsg.originAP = (bus_id << 6); + writeMsg.size = 2; + writeMsg.setWord(MFID_1_Storage, 0); + const u32 phase = 4; + const u32 locationWord = (block << 24) | (phase << 8); + writeMsg.setWord(locationWord, 1); + MapleMsg rcvMsg; + if (dreamlink->send(writeMsg, rcvMsg) && rcvMsg.command == MDRS_DeviceReply) { + // Acknowledged + break; + } + } + // else: continue to retry + } + + if (!writeSuccess) { + ERROR_LOG(MAPLE, "Failed to save VMU %s: I/O error", logical_port); + + const u64 currentTime = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + + if ((currentTime - lastErrorNotifyTime) > 4000) + { + os_notify("ATTENTION: Write to VMU failed", 6000); + lastErrorNotifyTime = currentTime; + } + } + } } }; -struct DreamConnPurupuru : public maple_sega_purupuru -{ - std::shared_ptr dreamconn; +u64 DreamLinkVmu::lastNotifyTime = 0; +u64 DreamLinkVmu::lastErrorNotifyTime = 0; - DreamConnPurupuru(std::shared_ptr dreamconn) : dreamconn(dreamconn) { +struct DreamLinkPurupuru : public maple_sega_purupuru +{ + std::shared_ptr dreamlink; + + //! Number of consecutive stop conditions sent + u32 stopSendCount = 0; + + DreamLinkPurupuru(std::shared_ptr dreamlink) : dreamlink(dreamlink) { } u32 dma(u32 cmd) override { if (cmd == MDCF_BlockWrite || cmd == MDCF_SetCondition) { const MapleMsg *msg = reinterpret_cast(dma_buffer_in - 4); - dreamconn->send(*msg); + const u32 functionId = *(u32*)dma_buffer_in; + const u32 condition = *(u32*)(dma_buffer_in + 4); + if (functionId == MFID_8_Vibration && condition == 0x00000010) { + ++stopSendCount; + } else { + stopSendCount = 0; + } + + // Only send 2 consecutive stop commands; ignore the rest to avoid unnecessary communications + if (stopSendCount <= 2) { + dreamlink->send(*msg); + } } return maple_sega_purupuru::dma(cmd); } }; -static std::list> dreamConnVmus; -static std::list> dreamConnPurupurus; +static std::list> dreamLinkVmus[2]; +static std::list> dreamLinkPurupurus; -void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart) +static void disablePhysicalVmuMemoryOption() { - const int bus = dreamconn->getBus(); - - bool vmuFound = false; - bool rumbleFound = false; - - if (dreamconn->hasVmu()) + // Make setting read only + config::UsePhysicalVmuMemory.override(config::UsePhysicalVmuMemory); +} + +static void enablePhysicalVmuMemoryOption() +{ + // Remove read-only setting and preserve current value + bool val = config::UsePhysicalVmuMemory; + config::UsePhysicalVmuMemory.reset(); + config::UsePhysicalVmuMemory.set(val); +} + +void createDreamLinkDevices(std::shared_ptr dreamlink, bool gameStart, bool gameEnd) +{ + const int bus = dreamlink->getBus(); + + for (int i = 0; i < 2; ++i) { - std::shared_ptr vmu; - for (const std::shared_ptr& vmuIter : dreamConnVmus) + std::shared_ptr dev = MapleDevices[bus][i]; + + if ((dreamlink->getFunctionCode(i + 1) & MFID_1_Storage) || (dev != nullptr && dev->get_device_type() == MDT_SegaVMU)) { - if (vmuIter->dreamconn.get() == dreamconn.get()) + bool vmuFound = false; + std::shared_ptr vmu; + for (const std::shared_ptr& vmuIter : dreamLinkVmus[i]) { - vmuFound = true; - vmu = vmuIter; - break; - } - } - - std::shared_ptr dev = MapleDevices[bus][0]; - if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_SegaVMU)) - { - bool vmuCreated = false; - if (!vmu) - { - vmu = std::make_shared(dreamconn); - vmuCreated = true; - } - - vmu->Setup(bus, 0); - - if ((!gameStart || !vmuCreated) && dev) { - // if loading a state or DreamConnVmu existed, copy data from the regular vmu and send a screen update - vmu->copyIn(std::static_pointer_cast(dev)); - if (!gameStart) { - vmu->updateScreen(); + if (vmuIter->dreamlink.get() == dreamlink.get()) + { + vmuFound = true; + vmu = vmuIter; + break; } } - if (!vmuFound) dreamConnVmus.push_back(vmu); - } - } - if (dreamconn->hasRumble()) - { - std::shared_ptr rumble; - for (const std::shared_ptr& purupuru : dreamConnPurupurus) - { - if (purupuru->dreamconn.get() == dreamconn.get()) + if (gameStart || !vmuFound) { - rumbleFound = true; - rumble = purupuru; - break; - } - } + if (!vmu) + { + vmu = std::make_shared(dreamlink); + } - std::shared_ptr dev = MapleDevices[bus][1]; - if (gameStart || (dev != nullptr && dev->get_device_type() == MDT_PurupuruPack)) - { - if (!rumble) - { - rumble = std::make_shared(dreamconn); + vmu->Setup(bus, i); + + if (!vmuFound && dev && dev->get_device_type() == MDT_SegaVMU && !vmu->useRealVmuMemory) { + // Only copy data from virtual VMU if Physical VMU Only is disabled + vmu->copyIn(std::static_pointer_cast(dev)); + if (!gameStart) { + vmu->updateScreen(); + } + } + + if (!vmuFound) { + dreamLinkVmus[i].push_back(vmu); + } + } + + if (gameStart) + { + disablePhysicalVmuMemoryOption(); + } + else if (gameEnd) + { + enablePhysicalVmuMemoryOption(); + } + } + else if (i == 1 && ((dreamlink->getFunctionCode(i + 1) & MFID_8_Vibration) || (dev != nullptr && dev->get_device_type() == MDT_PurupuruPack))) + { + bool rumbleFound = false; + std::shared_ptr rumble; + for (const std::shared_ptr& purupuru : dreamLinkPurupurus) + { + if (purupuru->dreamlink.get() == dreamlink.get()) + { + rumbleFound = true; + rumble = purupuru; + break; + } + } + + if (gameStart || !rumbleFound) + { + if (!rumble) + { + rumble = std::make_shared(dreamlink); + } + rumble->Setup(bus, i); + + if (!rumbleFound) dreamLinkPurupurus.push_back(rumble); } - rumble->Setup(bus, 1); - - if (!rumbleFound) dreamConnPurupurus.push_back(rumble); } } } -void tearDownDreamConnDevices(std::shared_ptr dreamconn) +void tearDownDreamLinkDevices(std::shared_ptr dreamlink) { - const int bus = dreamconn->getBus(); - for (std::list>::const_iterator iter = dreamConnVmus.begin(); - iter != dreamConnVmus.end();) + const int bus = dreamlink->getBus(); + + for (int i = 0; i < 2; ++i) { - if ((*iter)->dreamconn.get() == dreamconn.get()) + for (std::list>::const_iterator iter = dreamLinkVmus[i].begin(); + iter != dreamLinkVmus[i].end();) { - std::shared_ptr dev = maple_Create(MDT_SegaVMU); - dev->Setup(bus, 0); - (*iter)->copyOut(std::static_pointer_cast(dev)); - iter = dreamConnVmus.erase(iter); - break; - } - else - { - ++iter; + if ((*iter)->dreamlink.get() == dreamlink.get()) + { + DEBUG_LOG(MAPLE, "VMU teardown - Physical VMU: %s", (*iter)->useRealVmuMemory ? "true" : "false"); + std::shared_ptr dev = maple_Create(MDT_SegaVMU); + dev->Setup(bus, 0); + if (!(*iter)->useRealVmuMemory) + { + (*iter)->copyOut(std::static_pointer_cast(dev)); + } + DEBUG_LOG(MAPLE, "VMU teardown - Copy completed"); + iter = dreamLinkVmus[i].erase(iter); + break; + } + else + { + ++iter; + } } } - for (std::list>::const_iterator iter = dreamConnPurupurus.begin(); - iter != dreamConnPurupurus.end();) + + std::size_t dreamLinkVmuCount = 0; + for (int i = 0; i < 2; ++i) { - if ((*iter)->dreamconn.get() == dreamconn.get()) + dreamLinkVmuCount += dreamLinkVmus[i].size(); + } + + if (dreamLinkVmuCount == 0) + { + enablePhysicalVmuMemoryOption(); + } + + for (std::list>::const_iterator iter = dreamLinkPurupurus.begin(); + iter != dreamLinkPurupurus.end();) + { + if ((*iter)->dreamlink.get() == dreamlink.get()) { std::shared_ptr dev = maple_Create(MDT_PurupuruPack); dev->Setup(bus, 1); - iter = dreamConnPurupurus.erase(iter); + iter = dreamLinkPurupurus.erase(iter); break; } else diff --git a/core/sdl/dreamconn.cpp b/core/sdl/dreamconn.cpp index 7c3a08a77..d014fb35a 100644 --- a/core/sdl/dreamconn.cpp +++ b/core/sdl/dreamconn.cpp @@ -26,12 +26,6 @@ #include #include #include -#include -#include -#include -#include -#include -#include #if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) #include @@ -42,844 +36,123 @@ #include #endif -void createDreamConnDevices(std::shared_ptr dreamconn, bool gameStart); -void tearDownDreamConnDevices(std::shared_ptr dreamconn); - -class DreamcastControllerConnection +static asio::error_code sendMsg(const MapleMsg& msg, asio::ip::tcp::iostream& stream) { -private: - MapleMsg connection_msg; + std::ostringstream s; + s.fill('0'); + s << std::hex << std::uppercase + << std::setw(2) << (u32)msg.command << " " + << std::setw(2) << (u32)msg.destAP << " " + << std::setw(2) << (u32)msg.originAP << " " + << std::setw(2) << (u32)msg.size; + const u32 sz = msg.getDataSize(); + for (u32 i = 0; i < sz; i++) + s << " " << std::setw(2) << (u32)msg.data[i]; + s << "\r\n"; -public: - DreamcastControllerConnection(const DreamcastControllerConnection&) = delete; + asio::ip::tcp::socket& sock = static_cast(stream.socket()); + asio::error_code ec; + asio::write(sock, asio::buffer(s.str()), ec); + return ec; +} - DreamcastControllerConnection() = default; - ~DreamcastControllerConnection() = default; - - std::optional connect(int bus){ - bool result = establishConnection(bus); - - if (!result) { - return std::nullopt; - } - - // Now get the controller configuration - connection_msg.command = MDCF_GetCondition; - connection_msg.destAP = (bus << 6) | 0x20; - connection_msg.originAP = bus << 6; - connection_msg.setData(MFID_0_Input); - - asio::error_code ec = sendMsg(connection_msg); - if (ec) - { - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); - disconnect(); - return std::nullopt; - } - if (!receiveMsg(connection_msg)) { - WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); - disconnect(); - return std::nullopt; - } - - onConnectComplete(); - - return connection_msg; - } - - virtual void disconnect() = 0; - virtual asio::error_code sendMsg(const MapleMsg& msg) = 0; - virtual bool receiveMsg(MapleMsg& msg) = 0; - virtual std::string getName() = 0; - virtual int getDefaultBus() { - // Value of -1 means to use enumeration order - return -1; - } - virtual void gameTermination() { - // Do nothing by default - } - -protected: - virtual bool establishConnection(int bus) = 0; - virtual void onConnectComplete() = 0; -}; - -class DreamConnConnection : public DreamcastControllerConnection +static bool receiveMsg(MapleMsg& msg, std::istream& stream) { - //! Base port of communication to DreamConn - static constexpr u16 BASE_PORT = 37393; - //! Stream to a DreamConn device - asio::ip::tcp::iostream iostream; - -public: - //! DreamConn VID:4457 PID:4443 - static constexpr const char* VID_PID_GUID = "5744000043440000"; - -public: - DreamConnConnection(const DreamConnConnection&) = delete; - - DreamConnConnection() = default; - - ~DreamConnConnection() { - disconnect(); - } - - bool establishConnection(int bus) override { -#if !defined(_WIN32) - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus); + std::string response; + if (!std::getline(stream, response)) return false; -#else - iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); - if (!iostream) { - WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, iostream.error().message().c_str()); - disconnect(); - return false; - } - iostream.expires_from_now(std::chrono::seconds(1)); - return true; -#endif - } - - void onConnectComplete() override { - iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow - } - - void disconnect() override { - if (iostream) { - iostream.close(); - } - } - - asio::error_code sendMsg(const MapleMsg& msg) override { - std::ostringstream s; - s.fill('0'); - s << std::hex << std::uppercase - << std::setw(2) << (u32)msg.command << " " - << std::setw(2) << (u32)msg.destAP << " " - << std::setw(2) << (u32)msg.originAP << " " - << std::setw(2) << (u32)msg.size; - const u32 sz = msg.getDataSize(); - for (u32 i = 0; i < sz; i++) - s << " " << std::setw(2) << (u32)msg.data[i]; - s << "\r\n"; - - asio::ip::tcp::socket& sock = static_cast(iostream.socket()); - asio::error_code ec; - asio::write(sock, asio::buffer(s.str()), ec); - return ec; - } - - bool receiveMsg(MapleMsg& msg) override { - std::string response; - - if (!std::getline(iostream, response)) - return false; - sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); - if ((msg.getDataSize() - 1) * 3 + 13 >= response.length()) - return false; - for (unsigned i = 0; i < msg.getDataSize(); i++) - sscanf(&response[i * 3 + 12], "%hhx", &msg.data[i]); - return !iostream.fail(); - + sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); + if ((msg.getDataSize() - 1) * 3 + 13 >= response.length()) return false; - } + for (unsigned i = 0; i < msg.getDataSize(); i++) + sscanf(&response[i * 3 + 12], "%hhx", &msg.data[i]); + return !stream.fail(); +} - std::string getName() override { - return "DreamConn+ / DreamConn S Controller"; - } -}; -class DreamPicoPortSerialHandler -{ - //! Asynchronous context for serial_handler - asio::io_context io_context; - //! Output buffer data for serial_handler - std::string serial_out_data; - //! Handles communication to DreamPicoPort - asio::serial_port serial_handler{io_context}; - //! Set to true while an async write is in progress with serial_handler - bool serial_write_in_progress = false; - //! Signaled when serial_write_in_progress transitions to false - std::condition_variable write_cv; - //! Mutex for write_cv and serializes access to serial_write_in_progress - std::mutex write_cv_mutex; - //! Input stream buffer from serial_handler - asio::streambuf serial_read_buffer; - //! Thread which runs the io_context - std::unique_ptr io_context_thread; - //! Contains queue of incoming lines from serial - std::list read_queue; - //! Signaled when data is in read_queue - std::condition_variable read_cv; - //! Mutex for read_cv and serializes access to read_queue - std::mutex read_cv_mutex; -public: - DreamPicoPortSerialHandler() { - // the serial port isn't ready at this point, so we need to sleep briefly - // we probably should have a better way to handle this - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - serial_handler = asio::serial_port(io_context); - io_context.reset(); - - std::string serial_device = ""; - - // use user-configured serial device if available, fallback to first available - serial_device = cfgLoadStr("input", "DreamPicoPortSerialDevice", ""); - if (!serial_device.empty()) - { - NOTICE_LOG(INPUT, "DreamPicoPort connecting to user-configured serial device: %s", serial_device.c_str()); - } else { - serial_device = getFirstSerialDevice(); - NOTICE_LOG(INPUT, "DreamPicoPort connecting to autoselected serial device: %s", serial_device.c_str()); - } - - asio::error_code ec; - serial_handler.open(serial_device, ec); - - if (ec || !serial_handler.is_open()) { - WARN_LOG(INPUT, "DreamPicoPort serial connection failed: %s", ec.message().c_str()); - disconnect(); - } else { - NOTICE_LOG(INPUT, "DreamPicoPort serial connection successful!"); - } - - // This must be done before the io_context is run because it will keep io_context from returning immediately - startSerialRead(); - - io_context_thread = std::make_unique([this](){contextThreadEnty();}); - } - - ~DreamPicoPortSerialHandler() { - disconnect(); - io_context_thread->join(); - } - - bool is_open() const { - return serial_handler.is_open(); - } - - asio::error_code sendCmd(const std::string& cmd, std::chrono::milliseconds timeout_ms) { - asio::error_code ec; - - if (!serial_handler.is_open()) { - return asio::error::not_connected; - } - - // Wait for last write to complete - std::unique_lock lock(write_cv_mutex); - const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; - if (!write_cv.wait_until(lock, expiration, [this](){return (!serial_write_in_progress || !serial_handler.is_open());})) - { - return asio::error::timed_out; - } - - // Check again before continuing - if (!serial_handler.is_open()) { - return asio::error::not_connected; - } - - serial_out_data = cmd; - - // Clear out the read buffer before writing next command - read_queue.clear(); - serial_write_in_progress = true; - asio::async_write( - serial_handler, - asio::buffer(serial_out_data), - asio::transfer_exactly(serial_out_data.size()), - [this](const asio::error_code& error, size_t bytes_transferred) - { - std::unique_lock lock(write_cv_mutex); - if (error) { - try - { - serial_handler.cancel(); - } - catch(const asio::system_error&) - { - // Ignore cancel errors - } - } - serial_write_in_progress = false; - write_cv.notify_all(); - } - ); - - return ec; - } - - asio::error_code sendMsg(const MapleMsg& msg, int hardware_bus, std::chrono::milliseconds timeout_ms) { - // Build serial_out_data string - // Need to message the hardware bus instead of the software bus - u8 hwDestAP = (hardware_bus << 6) | (msg.destAP & 0x3F); - u8 hwOriginAP = (hardware_bus << 6) | (msg.originAP & 0x3F); - - std::ostringstream s; - s << "X "; // 'X' prefix triggers flycast command parser - s.fill('0'); - s << std::hex << std::uppercase - << std::setw(2) << (u32)msg.command - << std::setw(2) << (u32)hwDestAP // override dest - << std::setw(2) << (u32)hwOriginAP // override origin - << std::setw(2) << (u32)msg.size; - const u32 sz = msg.getDataSize(); - for (u32 i = 0; i < sz; i++) { - s << std::setw(2) << (u32)msg.data[i]; - } - s << "\n"; - - return sendCmd(s.str(), timeout_ms); - } - - bool receiveCmd(std::string& cmd, std::chrono::milliseconds timeout_ms) - { - // Wait for at least 2 lines to be received (first line is echo back) - std::unique_lock lock(read_cv_mutex); - const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; - if (!read_cv.wait_until(lock, expiration, [this](){return ((read_queue.size() >= 2) || !serial_handler.is_open());})) - { - // Timeout - return false; - } - - if (read_queue.size() < 2) { - // Connection was closed before data could be received - return false; - } - - // discard the first message as we are interested in the second only which returns the controller configuration - cmd = std::move(read_queue.back()); - read_queue.clear(); - return true; - } - - bool receiveMsg(MapleMsg& msg, std::chrono::milliseconds timeout_ms) - { - std::string response; - if (!receiveCmd(response, timeout_ms)) { - return false; - } - - sscanf(response.c_str(), "%hhx %hhx %hhx %hhx", &msg.command, &msg.destAP, &msg.originAP, &msg.size); - - if (serial_handler.is_open()) { - return true; - } - else { - return false; - } - - return false; - } - -private: - void disconnect() - { - io_context.stop(); - - if (serial_handler.is_open()) { - try - { - serial_handler.cancel(); - } - catch(const asio::system_error&) - { - // Ignore cancel errors - } - } - - try - { - serial_handler.close(); - } - catch(const asio::system_error&) - { - // Ignore closing errors - } - } - - void contextThreadEnty() - { - // This context should never exit until disconnect due to read handler automatically rearming - io_context.run(); - } - static std::string getFirstSerialDevice() { - - // On Windows, we get the first serial device matching our VID/PID -#if defined(_WIN32) - HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES); - if (deviceInfoSet == INVALID_HANDLE_VALUE) { - return ""; - } - - SP_DEVINFO_DATA deviceInfoData; - deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); - - for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) { - DWORD dataType, bufferSize = 0; - SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, NULL, 0, &bufferSize); - - if (bufferSize > 0) { - std::vector buffer(bufferSize); - if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)buffer.data(), bufferSize, NULL)) { - std::string hardwareId(buffer.begin(), buffer.end()); - if (hardwareId.find("VID_1209") != std::string::npos && hardwareId.find("PID_2F07") != std::string::npos) { - HKEY deviceKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); - if (deviceKey != INVALID_HANDLE_VALUE) { - char portName[256]; - DWORD portNameSize = sizeof(portName); - if (RegQueryValueEx(deviceKey, "PortName", NULL, NULL, (LPBYTE)portName, &portNameSize) == ERROR_SUCCESS) { - RegCloseKey(deviceKey); - SetupDiDestroyDeviceInfoList(deviceInfoSet); - return std::string(portName); - } - RegCloseKey(deviceKey); - } - } - } - } - } - - SetupDiDestroyDeviceInfoList(deviceInfoSet); - return ""; -#endif - -#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) - // On MacOS/Linux, we get the first serial device matching the device prefix - std::string device_prefix = ""; - -#if defined(__linux__) - device_prefix = "ttyACM"; -#elif (defined(__APPLE__) && defined(TARGET_OS_MAC)) - device_prefix = "tty.usbmodem"; -#endif - - std::string path = "/dev/"; - DIR *dir; - struct dirent *ent; - if ((dir = opendir(path.c_str())) != NULL) { - while ((ent = readdir(dir)) != NULL) { - std::string device = ent->d_name; - if (device.find(device_prefix) != std::string::npos) { - closedir(dir); - return path + device; - } - } - closedir(dir); - } - return ""; -#endif - } - - void startSerialRead() - { - serialReadHandler(asio::error_code(), 0); - // Just to make sure initial data is cleared off of incoming buffer - io_context.poll_one(); - read_queue.clear(); - } - - void serialReadHandler(const asio::error_code& error, std::size_t size) - { - if (error) { - std::lock_guard lock(read_cv_mutex); - try - { - serial_handler.cancel(); - } - catch(const asio::system_error&) - { - // Ignore cancel errors - } - read_cv.notify_all(); - } - else { - // Rearm the read - asio::async_read_until( - serial_handler, - serial_read_buffer, - '\n', - [this](const asio::error_code& error, std::size_t size) -> void { - if (size > 0) - { - // Lock access to read_queue - std::lock_guard lock(read_cv_mutex); - // Consume the received data - if (consumeReadBuffer() > 0) - { - // New lines available - read_cv.notify_all(); - } - } - // Auto reload read - io_context will always have work to do - serialReadHandler(error, size); - } - ); - } - } - - int consumeReadBuffer() { - if (serial_read_buffer.size() <= 0) { - return 0; - } - - int numberOfLines = 0; - while (true) - { - char c = '\0'; - std::string line; - - // Consume characters until buffers are empty or \n found - asio::const_buffers_1 data = serial_read_buffer.data(); - std::size_t consumed = 0; - for (const asio::const_buffer& buff : data) - { - const char* buffDat = static_cast(buff.data()); - for (std::size_t i = 0; i < buff.size(); ++i) - { - c = *buffDat++; - ++consumed; - - if (c == '\n') { - // Stop reading now - break; - } - - line += c; - } - - if (c == '\n') { - // Stop reading now - break; - } - } - - if (c == '\n') { - serial_read_buffer.consume(consumed); - - // Remove carriage return if found and add this line to queue - if (line.size() > 0 && line[line.size() - 1] == '\r') { - line.pop_back(); - } - read_queue.push_back(std::move(line)); - - ++numberOfLines; - } - else { - // Ran out of data to consume - return numberOfLines; - } - } - } -}; - -//! See: https://github.com/OrangeFox86/DreamPicoPort -class DreamPicoPortConnection : public DreamcastControllerConnection -{ - //! The one and only serial port - static std::unique_ptr serial; - //! Number of devices using the above serial - static std::atomic connected_dev_count; - //! Current timeout in milliseconds - std::chrono::milliseconds timeout_ms; - //! The bus ID dictated by flycast - int software_bus = -1; - //! The bus index of the hardware connection which will differ from the software bus - int hardware_bus = -1; - //! true iff only a single devices was found when enumerating devices - bool is_single_device = true; - //! True when initial enumeration failed - bool is_hardware_bus_implied = true; - //! True once connection is established - bool connection_established = false; - -public: - //! Dreamcast Controller USB VID:1209 PID:2f07 - static constexpr const std::uint16_t VID = 0x1209; - static constexpr const std::uint16_t PID = 0x2f07; - static constexpr const char* VID_PID_GUID = "09120000072f0000"; - -public: - DreamPicoPortConnection(const DreamPicoPortConnection&) = delete; - DreamPicoPortConnection() = delete; - - DreamPicoPortConnection(int joystick_idx, SDL_Joystick* sdl_joystick) : - DreamcastControllerConnection() - { -#if defined(_WIN32) - // Workaround: Getting the instance ID here fixes some sort of L/R trigger bug in Windows dinput for some reason - (void)SDL_JoystickGetDeviceInstanceID(joystick_idx); -#endif - determineHardwareBus(joystick_idx, sdl_joystick); - } - - ~DreamPicoPortConnection(){ - disconnect(); - } - - int hardwareBus() const { - return hardware_bus; - } - - bool isHardwareBusImplied() const { - return is_hardware_bus_implied; - } - - bool isSingleDevice() const { - return is_single_device; - } - - bool establishConnection(int bus) override { - // Timeout is 1 second while establishing connection - timeout_ms = std::chrono::seconds(1); - - software_bus = bus; - - if (connection_established && serial) { - if (serial->is_open()) { - // This equipment is fixed to the hardware bus - the software bus isn't relevant - sendPort(); - return true; - } else { - disconnect(); - return false; - } - } - - ++connected_dev_count; - connection_established = true; - if (!serial) { - serial = std::make_unique(); - } - - if (serial && serial->is_open()) { - sendPort(); - return true; - } else { - disconnect(); - return false; - } - } - - void sendPort() { - if (connection_established && software_bus >= 0 && software_bus <= 3 && hardware_bus >=0 && hardware_bus <= 3) { - // This will update the displayed port letter on the screen - std::ostringstream s; - s << "XP "; // XP is flycast "set port" command - s << hardware_bus << " " << software_bus << "\n"; - serial->sendCmd(s.str(), timeout_ms); - // Don't really care about the response, just want to ensure it gets fully processed before continuing - std::string buffer; - serial->receiveCmd(buffer, timeout_ms); - } - } - - void onConnectComplete() override { - // Timeout is extended to 5 seconds for all other communication after connection - timeout_ms = std::chrono::seconds(5); - } - - void disconnect() override { - if (connection_established) { - connection_established = false; - if (--connected_dev_count == 0) { - // serial is no longer needed - serial.reset(); - } - } - } - - asio::error_code sendMsg(const MapleMsg& msg) override { - if (serial) { - return serial->sendMsg(msg, hardware_bus, timeout_ms); - } - - return asio::error::not_connected; - } - - bool receiveMsg(MapleMsg& msg) override { - if (serial) { - return serial->receiveMsg(msg, timeout_ms); - } - - return false; - } - - std::string getName() override { - std::string name = "DreamPicoPort"; - if (!is_hardware_bus_implied && !is_single_device) { - const char portChar = ('A' + hardware_bus); - name += " " + std::string(1, portChar); - } - return name; - } - - int getDefaultBus() override { - if (!is_hardware_bus_implied && !is_single_device) { - return hardware_bus; - } else { - // Value of -1 means to use enumeration order - return -1; - } - } - - void gameTermination() override { - // Reset screen to selected port - sendPort(); - } - -private: - void determineHardwareBus(int joystick_idx, SDL_Joystick* sdl_joystick) { - // This function determines what bus index to use when communicating with the hardware. -#if defined(_WIN32) - // This only works in Windows because the joystick_path is not given in other OSes - const char* joystick_name = SDL_JoystickName(sdl_joystick); - const char* joystick_path = SDL_JoystickPath(sdl_joystick); - - struct SDL_hid_device_info* devs = SDL_hid_enumerate(VID, PID); - if (devs) { - if (!devs->next) { - // Only single device found, so this is simple (host-1p firmware used) - hardware_bus = 0; - is_hardware_bus_implied = false; - is_single_device = true; - } else { - struct SDL_hid_device_info* it = devs; - struct SDL_hid_device_info* my_dev = nullptr; - - if (joystick_path) - { - while (it) - { - // Note: hex characters will be differing case, so case-insensitive cmp is needed - if (it->path && 0 == SDL_strcasecmp(it->path, joystick_path)) { - my_dev = it; - break; - } - it = it->next; - } - } - - if (my_dev) { - it = devs; - int count = 0; - if (my_dev->serial_number) { - while (it) { - if (it->serial_number && - 0 == wcscmp(it->serial_number, my_dev->serial_number)) - { - ++count; - } - it = it->next; - } - - if (count == 1) { - // Single device of this serial found - is_single_device = true; - hardware_bus = 0; - is_hardware_bus_implied = false; - } else { - is_single_device = false; - if (my_dev->release_number < 0x0102) { - // Interfaces go in decending order - hardware_bus = (count - (my_dev->interface_number % 4) - 1); - is_hardware_bus_implied = false; - } else { - // Version 1.02 of interface will make interfaces in ascending order - hardware_bus = (my_dev->interface_number % 4); - is_hardware_bus_implied = false; - } - } - } - } - } - SDL_hid_free_enumeration(devs); - } -#endif - - if (hardware_bus < 0) { - // The number of buttons gives a clue as to what index the controller is - int nbuttons = SDL_JoystickNumButtons(sdl_joystick); - - if (nbuttons >= 32 || nbuttons <= 27) { - // Older version of firmware or single player - hardware_bus = 0; - is_hardware_bus_implied = true; - is_single_device = true; - } - else { - hardware_bus = 31 - nbuttons; - is_hardware_bus_implied = false; - is_single_device = false; - } - } - } -}; - -// Define the static instances here -std::unique_ptr DreamPicoPortConnection::serial; -std::atomic DreamPicoPortConnection::connected_dev_count = 0; - -DreamConn::DreamConn(int bus, int dreamcastControllerType, int joystick_idx, SDL_Joystick* sdl_joystick) : - bus(bus), dreamcastControllerType(dreamcastControllerType) -{ - switch (dreamcastControllerType) - { - case TYPE_DREAMCONN: - dcConnection = std::make_unique(); - break; - - case TYPE_DREAMPICOPORT: - dcConnection = std::make_unique(joystick_idx, sdl_joystick); - break; - } +DreamConn::DreamConn(int bus) : bus(bus) { } DreamConn::~DreamConn() { disconnect(); } -int DreamConn::getDefaultBus() { - if (dcConnection) { - return dcConnection->getDefaultBus(); +bool DreamConn::send(const MapleMsg& msg) { + std::lock_guard lock(send_mutex); // Ensure thread safety for send operations + + asio::error_code ec; + + if (maple_io_connected) + ec = sendMsg(msg, iostream); + else + return false; + if (ec) { + maple_io_connected = false; + WARN_LOG(INPUT, "DreamcastController[%d] send failed: %s", bus, ec.message().c_str()); + disconnect(); + return false; } - return -1; + return true; +} + +bool DreamConn::send(const MapleMsg& txMsg, MapleMsg& rxMsg) { + std::lock_guard lock(send_mutex); // Ensure thread safety for send operations + + if (!send(txMsg)) { + return false; + } + return receiveMsg(rxMsg, iostream); } void DreamConn::changeBus(int newBus) { bus = newBus; } -std::string DreamConn::getName() { - if (dcConnection) { - return dcConnection->getName(); - } - return "Unknown DreamConn"; -} - -void DreamConn::connect() -{ - if (maple_io_connected) { - disconnect(); - } - +void DreamConn::connect() { maple_io_connected = false; - expansionDevs = 0; - if (!dcConnection) { + asio::error_code ec; + +#if !defined(_WIN32) + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: DreamConn+ / DreamConn S Controller supported on Windows only", bus); + return; +#endif + + iostream = asio::ip::tcp::iostream("localhost", std::to_string(BASE_PORT + bus)); + if (!iostream) { + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, iostream.error().message().c_str()); + disconnect(); return; } + iostream.expires_from_now(std::chrono::seconds(1)); - std::optional msg = dcConnection->connect(bus); - if (!msg) + + // Now get the controller configuration + MapleMsg msg; + msg.command = MDCF_GetCondition; + msg.destAP = (bus << 6) | 0x20; + msg.originAP = bus << 6; + msg.setData(MFID_0_Input); + + ec = sendMsg(msg, iostream); + if (ec) { + WARN_LOG(INPUT, "DreamcastController[%d] connection failed: %s", bus, ec.message().c_str()); + disconnect(); + return; + } + if (!receiveMsg(msg, iostream)) { + WARN_LOG(INPUT, "DreamcastController[%d] read timeout", bus); + disconnect(); return; } - expansionDevs = msg->originAP & 0x1f; + iostream.expires_from_now(std::chrono::duration::max()); // don't use a 64-bit based duration to avoid overflow + + expansionDevs = msg.originAP & 0x1f; config::MapleExpansionDevices[bus][0] = hasVmu() ? MDT_SegaVMU : MDT_None; config::MapleExpansionDevices[bus][1] = hasRumble() ? MDT_PurupuruPack : MDT_None; @@ -897,210 +170,13 @@ void DreamConn::connect() } } -void DreamConn::disconnect() -{ - if (!dcConnection) { - return; - } - - dcConnection->disconnect(); +void DreamConn::disconnect() { + if (iostream) + iostream.close(); maple_io_connected = false; NOTICE_LOG(INPUT, "Disconnected from DreamcastController[%d]", bus); } -bool DreamConn::send(const MapleMsg& msg) -{ - if (!dcConnection) { - return false; - } - - asio::error_code ec; - - if (maple_io_connected) - ec = dcConnection->sendMsg(msg); - else - return false; - if (ec) { - maple_io_connected = false; - WARN_LOG(INPUT, "DreamcastController[%d] send failed: %s", bus, ec.message().c_str()); - disconnect(); - return false; - } - return true; -} - -void DreamConn::gameTermination() -{ - if (dcConnection) { - dcConnection->gameTermination(); - } -} - -bool DreamConnGamepad::isDreamcastController(int deviceIndex) -{ - char guid_str[33] {}; - SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(deviceIndex), guid_str, sizeof(guid_str)); - NOTICE_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str, - guid_str[10], guid_str[11], guid_str[8], guid_str[9], - guid_str[18], guid_str[19], guid_str[16], guid_str[17]); - - // DreamConn VID:4457 PID:4443 - // Dreamcast Controller USB VID:1209 PID:2f07 - if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0 || - memcmp(DreamPicoPortConnection::VID_PID_GUID, guid_str + 8, 16) == 0) - { - NOTICE_LOG(INPUT, "Dreamcast controller found!"); - return true; - } - return false; -} - -DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) - : SDLGamepad(maple_port, joystick_idx, sdl_joystick) -{ - char guid_str[33] {}; - - SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(joystick_idx), guid_str, sizeof(guid_str)); - - // DreamConn VID:4457 PID:4443 - // Dreamcast Controller USB VID:1209 PID:2f07 - if (memcmp(DreamConnConnection::VID_PID_GUID, guid_str + 8, 16) == 0) - { - dreamconn = std::make_shared(maple_port, TYPE_DREAMCONN, joystick_idx, sdl_joystick); - } - else if (memcmp(DreamPicoPortConnection::VID_PID_GUID, guid_str + 8, 16) == 0) - { - dreamconn = std::make_shared(maple_port, TYPE_DREAMPICOPORT, joystick_idx, sdl_joystick); - } - - if (dreamconn) { - _name = dreamconn->getName(); - int defaultBus = dreamconn->getDefaultBus(); - if (defaultBus >= 0 && defaultBus < 4) { - set_maple_port(defaultBus); - } - } - - EventManager::listen(Event::Start, handleEvent, this); - EventManager::listen(Event::LoadState, handleEvent, this); - EventManager::listen(Event::Terminate, handleEvent, this); -} - -DreamConnGamepad::~DreamConnGamepad() { - EventManager::unlisten(Event::Start, handleEvent, this); - EventManager::unlisten(Event::LoadState, handleEvent, this); - EventManager::unlisten(Event::Terminate, handleEvent, this); - if (dreamconn) { - tearDownDreamConnDevices(dreamconn); - dreamconn.reset(); - } -} - -void DreamConnGamepad::set_maple_port(int port) -{ - if (dreamconn) { - if (port < 0 || port >= 4) { - dreamconn->disconnect(); - } - else if (dreamconn->getBus() != port) { - dreamconn->changeBus(port); - if (is_registered()) { - dreamconn->connect(); - } - } - } - SDLGamepad::set_maple_port(port); -} - -void DreamConnGamepad::registered() -{ - if (dreamconn) - { - dreamconn->connect(); - } -} - -void DreamConnGamepad::handleEvent(Event event, void *arg) -{ - DreamConnGamepad *gamepad = static_cast(arg); - if (gamepad->dreamconn != nullptr) - createDreamConnDevices(gamepad->dreamconn, event == Event::Start); - - if (gamepad->dreamconn != nullptr && event == Event::Terminate) - { - gamepad->dreamconn->gameTermination(); - } -} - -bool DreamConnGamepad::gamepad_btn_input(u32 code, bool pressed) -{ - if (!is_detecting_input() && input_mapper) - { - DreamcastKey key = input_mapper->get_button_id(0, code); - if (key == DC_BTN_START) { - startPressed = pressed; - checkKeyCombo(); - } - } - else { - startPressed = false; - } - return SDLGamepad::gamepad_btn_input(code, pressed); -} - -bool DreamConnGamepad::gamepad_axis_input(u32 code, int value) -{ - if (!is_detecting_input()) - { - if (code == leftTrigger) { - ltrigPressed = value > 0; - checkKeyCombo(); - } - else if (code == rightTrigger) { - rtrigPressed = value > 0; - checkKeyCombo(); - } - } - else { - ltrigPressed = false; - rtrigPressed = false; - } - return SDLGamepad::gamepad_axis_input(code, value); -} - -void DreamConnGamepad::checkKeyCombo() { - if (ltrigPressed && rtrigPressed && startPressed) - gui_open_settings(); -} - -#else // USE_DREAMCASTCONTROLLER - -void DreamConn::connect() { -} -void DreamConn::disconnect() { -} -void DreamConn::gameTermination() { -} - -bool DreamConnGamepad::isDreamcastController(int deviceIndex) { - return false; -} -DreamConnGamepad::DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) - : SDLGamepad(maple_port, joystick_idx, sdl_joystick) { -} -DreamConnGamepad::~DreamConnGamepad() { -} -void DreamConnGamepad::set_maple_port(int port) { - SDLGamepad::set_maple_port(port); -} -void DreamConnGamepad::registered() { -} -bool DreamConnGamepad::gamepad_btn_input(u32 code, bool pressed) { - return SDLGamepad::gamepad_btn_input(code, pressed); -} -bool DreamConnGamepad::gamepad_axis_input(u32 code, int value) { - return SDLGamepad::gamepad_axis_input(code, value); -} #endif diff --git a/core/sdl/dreamconn.h b/core/sdl/dreamconn.h index b6a69e6c7..54bd1463a 100644 --- a/core/sdl/dreamconn.h +++ b/core/sdl/dreamconn.h @@ -17,95 +17,68 @@ along with Flycast. If not, see . */ #pragma once -#include "types.h" -#include "emulator.h" -#include "sdl_gamepad.h" -#if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) -#define USE_DREAMCASTCONTROLLER 1 -#define TYPE_DREAMCONN 1 -#define TYPE_DREAMPICOPORT 2 -#include -#endif -#include +#include "dreamlink.h" -struct MapleMsg -{ - u8 command; - u8 destAP; - u8 originAP; - u8 size; - u8 data[1024]; - - u32 getDataSize() const { - return size * 4; - } - - template - void setData(const T& p) { - memcpy(data, &p, sizeof(T)); - this->size = (sizeof(T) + 3) / 4; - } -}; -static_assert(sizeof(MapleMsg) == 1028); - -class DreamConn -{ - int bus = -1; - const int dreamcastControllerType; #ifdef USE_DREAMCASTCONTROLLER - std::unique_ptr dcConnection; -#endif + +#include +#include + +class DreamConn : public DreamLink +{ + //! Base port of communication to DreamConn + static constexpr u16 BASE_PORT = 37393; + + int bus = -1; bool maple_io_connected = false; u8 expansionDevs = 0; + asio::ip::tcp::iostream iostream; + std::mutex send_mutex; public: - DreamConn(int bus, int dreamcastControllerType, int joystick_idx, SDL_Joystick* sdl_joystick); + //! DreamConn VID:4457 PID:4443 + static constexpr const char* VID_PID_GUID = "5744000043440000"; + +public: + DreamConn(int bus); ~DreamConn(); - bool send(const MapleMsg& msg); + bool send(const MapleMsg& msg) override; - // When called, do teardown stuff like reset screen - void gameTermination(); + bool send(const MapleMsg& txMsg, MapleMsg& rxMsg) override; - int getBus() const { + int getBus() const override { return bus; } - bool hasVmu() { + + u32 getFunctionCode(int forPort) const override { + if (forPort == 1 && hasVmu()) { + return 0x0E000000; + } + else if (forPort == 2 && hasRumble()) { + return 0x00010000; + } + return 0; + } + + bool hasVmu() const { return expansionDevs & 1; } - bool hasRumble() { + + bool hasRumble() const { return expansionDevs & 2; } - int getDefaultBus(); + void changeBus(int newBus) override; - void changeBus(int newBus); + std::string getName() const override { + return "DreamConn+ / DreamConn S Controller"; + } - std::string getName(); + void connect() override; - void connect(); - void disconnect(); + void disconnect() override; }; -class DreamConnGamepad : public SDLGamepad -{ -public: - DreamConnGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick); - ~DreamConnGamepad(); - - void set_maple_port(int port) override; - void registered() override; - bool gamepad_btn_input(u32 code, bool pressed) override; - bool gamepad_axis_input(u32 code, int value) override; - static bool isDreamcastController(int deviceIndex); - -private: - static void handleEvent(Event event, void *arg); - void checkKeyCombo(); - - std::shared_ptr dreamconn; - bool ltrigPressed = false; - bool rtrigPressed = false; - bool startPressed = false; -}; +#endif // USE_DREAMCASTCONTROLLER diff --git a/core/sdl/dreamlink.cpp b/core/sdl/dreamlink.cpp new file mode 100644 index 000000000..a6dc0979d --- /dev/null +++ b/core/sdl/dreamlink.cpp @@ -0,0 +1,222 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#include "dreamlink.h" + +#ifdef USE_DREAMCASTCONTROLLER + +#include "dreamconn.h" +#include "dreampicoport.h" + +#include "hw/maple/maple_devs.h" +#include "ui/gui.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) +#include +#endif + +#if defined(_WIN32) +#include +#include +#endif + +void createDreamLinkDevices(std::shared_ptr dreamlink, bool gameStart, bool gameEnd); +void tearDownDreamLinkDevices(std::shared_ptr dreamlink); + +bool DreamLinkGamepad::isDreamcastController(int deviceIndex) +{ + char guid_str[33] {}; + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(deviceIndex), guid_str, sizeof(guid_str)); + NOTICE_LOG(INPUT, "GUID: %s VID:%c%c%c%c PID:%c%c%c%c", guid_str, + guid_str[10], guid_str[11], guid_str[8], guid_str[9], + guid_str[18], guid_str[19], guid_str[16], guid_str[17]); + + // DreamConn VID:4457 PID:4443 + // Dreamcast Controller USB VID:1209 PID:2f07 + const char* pid_vid_guid_str = guid_str + 8; + if (memcmp(DreamConn::VID_PID_GUID, pid_vid_guid_str, 16) == 0 || + memcmp(DreamPicoPort::VID_PID_GUID, pid_vid_guid_str, 16) == 0) + { + NOTICE_LOG(INPUT, "Dreamcast controller found!"); + return true; + } + return false; +} + +DreamLinkGamepad::DreamLinkGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) + : SDLGamepad(maple_port, joystick_idx, sdl_joystick) +{ + char guid_str[33] {}; + + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(joystick_idx), guid_str, sizeof(guid_str)); + + // DreamConn VID:4457 PID:4443 + // Dreamcast Controller USB VID:1209 PID:2f07 + if (memcmp(DreamConn::VID_PID_GUID, guid_str + 8, 16) == 0) + { + dreamlink = std::make_shared(maple_port); + } + else if (memcmp(DreamPicoPort::VID_PID_GUID, guid_str + 8, 16) == 0) + { + dreamlink = std::make_shared(maple_port, joystick_idx, sdl_joystick); + } + + if (dreamlink) { + _name = dreamlink->getName(); + int defaultBus = dreamlink->getDefaultBus(); + if (defaultBus >= 0 && defaultBus < 4) { + set_maple_port(defaultBus); + } + + } + + EventManager::listen(Event::Start, handleEvent, this); + EventManager::listen(Event::LoadState, handleEvent, this); + EventManager::listen(Event::Terminate, handleEvent, this); +} + +DreamLinkGamepad::~DreamLinkGamepad() { + EventManager::unlisten(Event::Start, handleEvent, this); + EventManager::unlisten(Event::LoadState, handleEvent, this); + EventManager::unlisten(Event::Terminate, handleEvent, this); + if (dreamlink) { + tearDownDreamLinkDevices(dreamlink); + dreamlink.reset(); + + // Make sure settings are open in case disconnection happened mid-game + if (!gui_is_open()) { + gui_open_settings(); + } + } +} + +void DreamLinkGamepad::set_maple_port(int port) +{ + if (dreamlink) { + if (port < 0 || port >= 4) { + dreamlink->disconnect(); + } + else if (dreamlink->getBus() != port) { + dreamlink->changeBus(port); + if (is_registered()) { + dreamlink->connect(); + } + } + } + SDLGamepad::set_maple_port(port); +} + +void DreamLinkGamepad::registered() +{ + if (dreamlink) + { + dreamlink->connect(); + + // Create DreamLink Maple Devices here just in case game is already running + createDreamLinkDevices(dreamlink, false, false); + } +} + +void DreamLinkGamepad::handleEvent(Event event, void *arg) +{ + DreamLinkGamepad *gamepad = static_cast(arg); + if (gamepad->dreamlink != nullptr && event != Event::Terminate) { + createDreamLinkDevices(gamepad->dreamlink, event == Event::Start, event == Event::Terminate); + } + + if (gamepad->dreamlink != nullptr && event == Event::Terminate) + { + gamepad->dreamlink->gameTermination(); + } +} + +bool DreamLinkGamepad::gamepad_btn_input(u32 code, bool pressed) +{ + if (!is_detecting_input() && input_mapper) + { + DreamcastKey key = input_mapper->get_button_id(0, code); + if (key == DC_BTN_START) { + startPressed = pressed; + checkKeyCombo(); + } + } + else { + startPressed = false; + } + return SDLGamepad::gamepad_btn_input(code, pressed); +} + +bool DreamLinkGamepad::gamepad_axis_input(u32 code, int value) +{ + if (!is_detecting_input()) + { + if (code == leftTrigger) { + ltrigPressed = value > 0; + checkKeyCombo(); + } + else if (code == rightTrigger) { + rtrigPressed = value > 0; + checkKeyCombo(); + } + } + else { + ltrigPressed = false; + rtrigPressed = false; + } + return SDLGamepad::gamepad_axis_input(code, value); +} + +void DreamLinkGamepad::checkKeyCombo() { + if (ltrigPressed && rtrigPressed && startPressed) + gui_open_settings(); +} + +#else // USE_DREAMCASTCONTROLLER + +bool DreamLinkGamepad::isDreamcastController(int deviceIndex) { + return false; +} +DreamLinkGamepad::DreamLinkGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick) + : SDLGamepad(maple_port, joystick_idx, sdl_joystick) { +} +DreamLinkGamepad::~DreamLinkGamepad() { +} +void DreamLinkGamepad::set_maple_port(int port) { + SDLGamepad::set_maple_port(port); +} +void DreamLinkGamepad::registered() { +} +bool DreamLinkGamepad::gamepad_btn_input(u32 code, bool pressed) { + return SDLGamepad::gamepad_btn_input(code, pressed); +} +bool DreamLinkGamepad::gamepad_axis_input(u32 code, int value) { + return SDLGamepad::gamepad_axis_input(code, value); +} + +#endif diff --git a/core/sdl/dreamlink.h b/core/sdl/dreamlink.h new file mode 100644 index 000000000..304aa82da --- /dev/null +++ b/core/sdl/dreamlink.h @@ -0,0 +1,130 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once + +// This file contains abstraction layer for access to different kinds of physical controllers + +#include "types.h" +#include "emulator.h" +#include "sdl_gamepad.h" + +#include +#include + +#if (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC))) && !defined(TARGET_UWP) +#define USE_DREAMCASTCONTROLLER 1 +#endif + +#include + +struct MapleMsg +{ + u8 command = 0; + u8 destAP = 0; + u8 originAP = 0; + u8 size = 0; + u8 data[1024]; + + u32 getDataSize() const { + return size * 4; + } + + template + void setData(const T& p) { + memcpy(data, &p, sizeof(T)); + this->size = (sizeof(T) + 3) / 4; + } + + void setWord(const u32& p, int index) { + if (index < 0 || index >= 256) { + return; + } + memcpy(&data[index * 4], &p, sizeof(u32)); + if (this->size <= index) { + this->size = index + 1; + } + } +}; +static_assert(sizeof(MapleMsg) == 1028); + +// Abstract base class for physical controller implementations +class DreamLink +{ +public: + DreamLink() = default; + + virtual ~DreamLink() = default; + + //! Sends a message to the controller, ignoring the response + //! @note The implementation shall be thread safe + virtual bool send(const MapleMsg& msg) = 0; + + //! Sends a message to the controller and waits for a response + //! @note The implementation shall be thread safe + virtual bool send(const MapleMsg& txMsg, MapleMsg& rxMsg) = 0; + + //! When called, do teardown stuff like reset screen + virtual inline void gameTermination() {} + + //! @param[in] forPort The port number to get the function code of (1 or 2) + //! @return the device type for the given port + virtual u32 getFunctionCode(int forPort) const = 0; + + //! @return the default bus number to select for this controller or -1 to not select a default + virtual int getDefaultBus() const { + return -1; + } + + //! @return the selected bus number of the controller + virtual int getBus() const = 0; + + //! Changes the selected maple port is changed by the user + virtual void changeBus(int newBus) = 0; + + //! @return the display name of the controller + virtual std::string getName() const = 0; + + //! Attempt connection to the hardware controller + virtual void connect() = 0; + + //! Disconnect from the hardware controller + virtual void disconnect() = 0; +}; + +class DreamLinkGamepad : public SDLGamepad +{ +public: + DreamLinkGamepad(int maple_port, int joystick_idx, SDL_Joystick* sdl_joystick); + ~DreamLinkGamepad(); + + void set_maple_port(int port) override; + void registered() override; + bool gamepad_btn_input(u32 code, bool pressed) override; + bool gamepad_axis_input(u32 code, int value) override; + static bool isDreamcastController(int deviceIndex); + +private: + static void handleEvent(Event event, void *arg); + void checkKeyCombo(); + + std::shared_ptr dreamlink; + bool ltrigPressed = false; + bool rtrigPressed = false; + bool startPressed = false; +}; diff --git a/core/sdl/dreampicoport.cpp b/core/sdl/dreampicoport.cpp new file mode 100644 index 000000000..3f19e6af1 --- /dev/null +++ b/core/sdl/dreampicoport.cpp @@ -0,0 +1,1035 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ + +#include "dreampicoport.h" + +#ifdef USE_DREAMCASTCONTROLLER +#include "hw/maple/maple_devs.h" +#include "ui/gui.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) +#include +#endif + +#if defined(_WIN32) +#include +#include +#endif + +class DreamPicoPortSerialHandler +{ + //! Asynchronous context for serial_handler + asio::io_context io_context; + //! Output buffer data for serial_handler + std::string serial_out_data; + //! Handles communication to DreamPicoPort + asio::serial_port serial_handler{io_context}; + //! Set to true while an async write is in progress with serial_handler + bool serial_write_in_progress = false; + //! Set to true while an async read is in progress with serial_handler + std::atomic serial_read_in_progress = false; + //! Signaled when serial_write_in_progress transitions to false + std::condition_variable write_cv; + //! Mutex for write_cv and serializes access to serial_write_in_progress + std::mutex write_cv_mutex; + //! Input stream buffer from serial_handler + char serial_read_buffer[1024]; + //! Holds on to partially parsed line + std::string read_line_buffer; + //! Thread which runs the io_context + std::unique_ptr io_context_thread; + //! Contains queue of incoming lines from serial + std::list read_queue; + //! Signaled when data is in read_queue + std::condition_variable read_cv; + //! Mutex for read_cv and serializes access to read_queue + std::mutex read_cv_mutex; + + //! When >= 0, parsing binary input and signifies total number parsed in this set + //! When < 0, not parsing binary input + int32_t num_binary_parsed = -1; + //! Number of binary bytes left to parse + uint16_t stored_binary_size = 0; + //! Number of binary bytes left to parse in current set + uint16_t num_binary_left = 0; + + //! Serializes send calls, making them thread-safe + std::mutex send_mutex; + +public: + DreamPicoPortSerialHandler() { + + // the serial port isn't ready at this point, so we need to sleep briefly + // we probably should have a better way to handle this + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + serial_handler = asio::serial_port(io_context); + io_context.reset(); + + std::string serial_device = ""; + + // use user-configured serial device if available, fallback to first available + serial_device = cfgLoadStr("input", "DreamPicoPortSerialDevice", ""); + if (!serial_device.empty()) + { + NOTICE_LOG(INPUT, "DreamPicoPort connecting to user-configured serial device: %s", serial_device.c_str()); + } else { + serial_device = getFirstSerialDevice(); + NOTICE_LOG(INPUT, "DreamPicoPort connecting to autoselected serial device: %s", serial_device.c_str()); + } + + asio::error_code ec; + serial_handler.open(serial_device, ec); + + if (ec || !serial_handler.is_open()) { + WARN_LOG(INPUT, "DreamPicoPort serial connection failed: %s", ec.message().c_str()); + disconnect(); + } else { + NOTICE_LOG(INPUT, "DreamPicoPort serial connection successful!"); + } + + // This must be done before the io_context is run because it will keep io_context from returning immediately + startSerialRead(); + + io_context_thread = std::make_unique([this](){contextThreadEnty();}); + } + + ~DreamPicoPortSerialHandler() { + disconnect(); + io_context_thread->join(); + } + + bool is_open() const { + return serial_handler.is_open(); + } + + asio::error_code sendCmd( + const std::string& cmd, + std::string& response, + std::chrono::milliseconds timeout_ms + ) { + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + + std::lock_guard lock(send_mutex); // Ensure thread safety for send operations + + asio::error_code ec = transmit(cmd, true, expiration); + + if (!ec) { + ec = receive(response, expiration); + } + + return ec; + } + + asio::error_code sendCmd( + const std::string& cmd, + std::chrono::milliseconds timeout_ms + ) { + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + + std::lock_guard lock(send_mutex); // Ensure thread safety for send operations + + return transmit(cmd, false, expiration); + } + + asio::error_code sendMsg( + const MapleMsg& msg, + int hardware_bus, + MapleMsg& response, + std::chrono::milliseconds timeout_ms) + { + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + + std::lock_guard lock(send_mutex); // Ensure thread safety for send operations + + std::string cmd = msgToStr(msg, hardware_bus); + asio::error_code ec = transmit(cmd, true, expiration); + + if (!ec) { + ec = receive(response, expiration); + } + + return ec; + } + + asio::error_code sendMsg( + const MapleMsg& msg, + int hardware_bus, + std::chrono::milliseconds timeout_ms) + { + const std::chrono::steady_clock::time_point expiration = std::chrono::steady_clock::now() + timeout_ms; + + std::lock_guard lock(send_mutex); // Ensure thread safety for send operations + + std::string cmd = msgToStr(msg, hardware_bus); + return transmit(cmd, false, expiration); + } + +private: + void disconnect() + { + io_context.stop(); + + if (serial_handler.is_open()) { + try + { + serial_handler.cancel(); + } + catch(const asio::system_error&) + { + // Ignore cancel errors + } + } + + try + { + serial_handler.close(); + } + catch(const asio::system_error&) + { + // Ignore closing errors + } + } + + void contextThreadEnty() + { + // This context should never exit until disconnect due to read handler automatically rearming + io_context.run(); + } + + static std::string getFirstSerialDevice() { + + // On Windows, we get the first serial device matching our VID/PID +#if defined(_WIN32) + HDEVINFO deviceInfoSet = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_PRESENT | DIGCF_ALLCLASSES); + if (deviceInfoSet == INVALID_HANDLE_VALUE) { + return ""; + } + + SP_DEVINFO_DATA deviceInfoData; + deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); + + for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &deviceInfoData); ++i) { + DWORD dataType, bufferSize = 0; + SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, NULL, 0, &bufferSize); + + if (bufferSize > 0) { + std::vector buffer(bufferSize); + if (SetupDiGetDeviceRegistryProperty(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)buffer.data(), bufferSize, NULL)) { + std::string hardwareId(buffer.begin(), buffer.end()); + if (hardwareId.find("VID_1209") != std::string::npos && hardwareId.find("PID_2F07") != std::string::npos) { + HKEY deviceKey = SetupDiOpenDevRegKey(deviceInfoSet, &deviceInfoData, DICS_FLAG_GLOBAL, 0, DIREG_DEV, KEY_READ); + if (deviceKey != INVALID_HANDLE_VALUE) { + char portName[256]; + DWORD portNameSize = sizeof(portName); + if (RegQueryValueEx(deviceKey, "PortName", NULL, NULL, (LPBYTE)portName, &portNameSize) == ERROR_SUCCESS) { + RegCloseKey(deviceKey); + SetupDiDestroyDeviceInfoList(deviceInfoSet); + return std::string(portName); + } + RegCloseKey(deviceKey); + } + } + } + } + } + + SetupDiDestroyDeviceInfoList(deviceInfoSet); + return ""; +#endif + +#if defined(__linux__) || (defined(__APPLE__) && defined(TARGET_OS_MAC)) + // On MacOS/Linux, we get the first serial device matching the device prefix + std::string device_prefix = ""; + +#if defined(__linux__) + device_prefix = "ttyACM"; +#elif (defined(__APPLE__) && defined(TARGET_OS_MAC)) + device_prefix = "tty.usbmodem"; +#endif + + std::string path = "/dev/"; + DIR *dir; + struct dirent *ent; + if ((dir = opendir(path.c_str())) != NULL) { + while ((ent = readdir(dir)) != NULL) { + std::string device = ent->d_name; + if (device.find(device_prefix) != std::string::npos) { + closedir(dir); + return path + device; + } + } + closedir(dir); + } + return ""; +#endif + } + + asio::error_code transmit( + const std::string& cmd, + bool receive_expected, + const std::chrono::steady_clock::time_point& expiration + ) { + asio::error_code ec; + + if (!serial_handler.is_open()) { + return asio::error::not_connected; + } + + if (receive_expected && serial_read_in_progress) { + // Wait up to 30 ms for read to complete before writing to help ensure expected command order. + // Continue regardless of result. + std::string rx; + + std::chrono::steady_clock::time_point rxExpiration = + std::chrono::steady_clock::now() + std::chrono::milliseconds(30); + + if (rxExpiration > expiration) { + rxExpiration = expiration; + } + + (void)receive(rx, rxExpiration); + } else { + // Just clear out the read queue before continuing + std::unique_lock lock(read_cv_mutex); + read_queue.clear(); + } + + // Wait for last write to complete + std::unique_lock lock(write_cv_mutex); + if (!write_cv.wait_until(lock, expiration, [this](){return (!serial_write_in_progress || !serial_handler.is_open());})) + { + return asio::error::timed_out; + } + + // Check again before continuing + if (!serial_handler.is_open()) { + return asio::error::not_connected; + } + + serial_out_data = cmd; + + // Clear out the read buffer before writing next command + serial_write_in_progress = true; + serial_read_in_progress = true; + asio::async_write( + serial_handler, + asio::buffer(serial_out_data), + asio::transfer_exactly(serial_out_data.size()), + [this](const asio::error_code& error, size_t bytes_transferred) + { + std::unique_lock lock(write_cv_mutex); + if (error) { + try + { + serial_handler.cancel(); + } + catch(const asio::system_error&) + { + // Ignore cancel errors + } + } + serial_write_in_progress = false; + write_cv.notify_all(); + } + ); + + return ec; + } + + asio::error_code receive(std::string& cmd, const std::chrono::steady_clock::time_point& expiration) + { + asio::error_code ec; + + // Wait for at least 2 lines to be received (first line is echo back) + std::unique_lock lock(read_cv_mutex); + if (!read_cv.wait_until(lock, expiration, [this](){return ((read_queue.size() >= 2) || !serial_handler.is_open());})) + { + // Timeout + return asio::error::timed_out; + } + + if (read_queue.size() < 2) { + // Connection was closed before data could be received + return asio::error::connection_aborted; + } + + // discard the first message as we are interested in the second only which returns the controller configuration + cmd = std::move(read_queue.back()); + read_queue.clear(); + serial_read_in_progress = false; + return ec; + } + + asio::error_code receive(MapleMsg& msg, const std::chrono::steady_clock::time_point& expiration) + { + asio::error_code ec; + std::string response; + + ec = receive(response, expiration); + if (ec) { + return ec; + } + + std::vector words; + bool valid = false; + const char* iter = response.c_str(); + const char* eol = iter + response.size(); + + if (*iter == '*') + { + // Asterisk indicates the write or read operation failed + return asio::error::no_data; + } + else if (*iter == '\5') // binary parsing + { + // binary + ++iter; + while (iter < eol) + { + uint32_t word = 0; + uint32_t i = 0; + while (i < 4 && iter < eol) + { + const u8* pu8 = reinterpret_cast(iter++); + // Apply value into current word + word |= (*pu8 << ((4 - i) * 8 - 8)); + ++i; + } + + // Invalid if a partial word was given + valid = ((i == 4) || (i == 0)); + + if (i == 4) + { + words.push_back(word); + } + } + } + else + { + while (iter < eol) + { + uint32_t word = 0; + uint32_t i = 0; + while (i < 8 && iter < eol) + { + char v = *iter++; + uint_fast8_t value = 0; + + if (v >= '0' && v <= '9') + { + value = v - '0'; + } + else if (v >= 'a' && v <= 'f') + { + value = v - 'a' + 0xa; + } + else if (v >= 'A' && v <= 'F') + { + value = v - 'A' + 0xA; + } + else + { + // Ignore this character + continue; + } + + // Apply value into current word + word |= (value << ((8 - i) * 4 - 4)); + ++i; + } + + // Invalid if a partial word was given + valid = ((i == 8) || (i == 0)); + + if (i == 8) + { + words.push_back(word); + } + } + } + + if (words.size() > 0) + { + msg.command = (words[0] >> 24) & 0xFF; + msg.destAP = (words[0] >> 16) & 0xFF; + msg.originAP = (words[0] >> 8) & 0xFF; + msg.size = words[0] & 0xFF; + + for (uint32_t i = 1; i < words.size(); ++i) + { + uint32_t dat = ntohl(words[i]); + memcpy(&msg.data[(i-1)*4], &dat, sizeof(dat)); + } + } + else + { + return asio::error::message_size; + } + + if (!serial_handler.is_open()) { + return asio::error::not_connected; + } + + return ec; + } + + std::string msgToStr(const MapleMsg& msg, int hardware_bus) { + // Build serial_out_data string + // Need to message the hardware bus instead of the software bus + u8 hwDestAP = (hardware_bus << 6) | (msg.destAP & 0x3F); + u8 hwOriginAP = (hardware_bus << 6) | (msg.originAP & 0x3F); + + std::ostringstream s; + s << "X "; // 'X' prefix triggers flycast command parser + s.fill('0'); + s << std::hex << std::uppercase + << std::setw(2) << (u32)msg.command + << std::setw(2) << (u32)hwDestAP // override dest + << std::setw(2) << (u32)hwOriginAP // override origin + << std::setw(2) << (u32)msg.size; + const u32 sz = msg.getDataSize(); + for (u32 i = 0; i < sz; i++) { + s << std::setw(2) << (u32)msg.data[i]; + } + s << "\n"; + + return s.str(); + } + + void startSerialRead() + { + serialReadHandler(); + // Just to make sure initial data is cleared off of incoming buffer + io_context.poll_one(); + read_queue.clear(); + } + + void serialReadHandler() + { + // Arm or rearm the read + serial_handler.async_read_some( + asio::buffer(serial_read_buffer, sizeof(serial_read_buffer)), + [this](const asio::error_code& error, std::size_t size) -> void { + std::lock_guard lock(read_cv_mutex); + if (error) { + try + { + serial_handler.cancel(); + } + catch(const asio::system_error&) + { + // Ignore cancel errors + } + read_cv.notify_all(); + } else { + if (size > 0) { + // Consume the received data + if (consumeReadBuffer(size) > 0) + { + // New lines available + read_cv.notify_all(); + } + } + // Auto reload read - io_context will always have work to do + serialReadHandler(); + } + } + ); + } + + int consumeReadBuffer(std::size_t size) { + if (size <= 0) { + return 0; + } + + int numberOfLines = 0; + const char* iter = serial_read_buffer; + while (size-- > 0) + { + char c = *iter++; + + if (num_binary_parsed >= 0) + { + ++num_binary_parsed; + --num_binary_left; + + if (num_binary_parsed == 1) + { + stored_binary_size = (c << 8); + } + else if (num_binary_parsed == 2) + { + stored_binary_size |= c; + num_binary_left = stored_binary_size; + read_line_buffer.reserve(1 + stored_binary_size); + } + else + { + read_line_buffer += c; + } + + if (num_binary_left == 0) + { + num_binary_parsed = -1; + } + } + else if (c == '\5') // binary start character + { + read_line_buffer += c; + num_binary_parsed = 0; + stored_binary_size = 0; + num_binary_left = 2; // Parse size + } + else if (c == '\n') + { + // Remove carriage return if found and add this line to queue + if (read_line_buffer.size() > 0 && read_line_buffer[read_line_buffer.size() - 1] == '\r') { + read_line_buffer.pop_back(); + } + read_queue.push_back(read_line_buffer); + read_line_buffer.clear(); + + ++numberOfLines; + } + else + { + read_line_buffer += c; + } + } + + return numberOfLines; + } +}; + +// Define the static instances here +std::unique_ptr DreamPicoPort::serial; +std::atomic DreamPicoPort::connected_dev_count = 0; + +DreamPicoPort::DreamPicoPort(int bus, int joystick_idx, SDL_Joystick* sdl_joystick) : + software_bus(bus) +{ +#if defined(_WIN32) + // Workaround: Getting the instance ID here fixes some sort of L/R trigger bug in Windows dinput for some reason + (void)SDL_JoystickGetDeviceInstanceID(joystick_idx); +#endif + determineHardwareBus(joystick_idx, sdl_joystick); +} + +DreamPicoPort::~DreamPicoPort() { + disconnect(); +} + +bool DreamPicoPort::send(const MapleMsg& msg) { + if (serial) { + asio::error_code ec = serial->sendMsg(msg, hardware_bus, timeout_ms); + return !ec; + } + + return false; +} + +bool DreamPicoPort::send(const MapleMsg& txMsg, MapleMsg& rxMsg) { + if (serial) { + asio::error_code ec = serial->sendMsg(txMsg, hardware_bus, rxMsg, timeout_ms); + return !ec; + } + + return false; +} + +inline void DreamPicoPort::gameTermination() { + // Need a short delay to wait for last screen draw to complete + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + // Reset screen to selected port + sendPort(); +} + +int DreamPicoPort::getBus() const { + return software_bus; +} + +u32 DreamPicoPort::getFunctionCode(int forPort) const { + u32 mask = 0; + if (peripherals.size() > forPort) { + for (const auto& peripheral : peripherals[forPort]) { + mask |= peripheral[0]; + } + } + // swap bytes to get the correct function code + return SWAP32(mask); +} + +int DreamPicoPort::getDefaultBus() const { + if (!is_hardware_bus_implied && !is_single_device) { + return hardware_bus; + } else { + // Value of -1 means to use enumeration order + return -1; + } +} + +void DreamPicoPort::changeBus(int newBus) { + software_bus = newBus; +} + +std::string DreamPicoPort::getName() const { + std::string name = "DreamPicoPort"; + if (!is_hardware_bus_implied && !is_single_device) { + const char portChar = ('A' + hardware_bus); + name += " " + std::string(1, portChar); + } + return name; +} + +void DreamPicoPort::connect() { + // Timeout is 1 second while establishing connection + timeout_ms = std::chrono::seconds(1); + + if (connection_established && serial) { + if (serial->is_open()) { + sendPort(); + } else { + disconnect(); + return; + } + } + + ++connected_dev_count; + connection_established = true; + if (!serial) { + serial = std::make_unique(); + } + + if (serial && serial->is_open()) { + sendPort(); + } else { + disconnect(); + return; + } + + if (!queryInterfaceVersion()) { + disconnect(); + return; + } + + if (!queryPeripherals()) { + disconnect(); + return; + } + + // Timeout is extended to 5 seconds for all other communication after connection + timeout_ms = std::chrono::seconds(5); + + int vmuCount = 0; + int vibrationCount = 0; + + u32 portOneFn = getFunctionCode(1); + if (portOneFn & MFID_1_Storage) { + config::MapleExpansionDevices[software_bus][0] = MDT_SegaVMU; + ++vmuCount; + } + else { + config::MapleExpansionDevices[software_bus][0] = MDT_None; + } + + u32 portTwoFn = getFunctionCode(2); + if (portTwoFn & MFID_8_Vibration) { + config::MapleExpansionDevices[software_bus][1] = MDT_PurupuruPack; + ++vibrationCount; + } + else if (portTwoFn & MFID_1_Storage) { + config::MapleExpansionDevices[software_bus][1] = MDT_SegaVMU; + ++vmuCount; + } + else { + config::MapleExpansionDevices[software_bus][1] = MDT_None; + } + + NOTICE_LOG(INPUT, "Connected to DreamcastController[%d]: Type:%s, VMU:%d, Rumble Pack:%d", software_bus, getName().c_str(), vmuCount, vibrationCount); +} + +void DreamPicoPort::disconnect() { + if (connection_established) { + connection_established = false; + if (--connected_dev_count == 0) { + // serial is no longer needed + serial.reset(); + } + } +} + +void DreamPicoPort::sendPort() { + if (connection_established && serial && software_bus >= 0 && software_bus <= 3 && hardware_bus >=0 && hardware_bus <= 3) { + // This will update the displayed port letter on the screen + std::ostringstream s; + s << "XP "; // XP is flycast "set port" command + s << hardware_bus << " " << software_bus << "\n"; + serial->sendCmd(s.str(), timeout_ms); + } +} + +int DreamPicoPort::hardwareBus() const { + return hardware_bus; +} + +bool DreamPicoPort::isHardwareBusImplied() const { + return is_hardware_bus_implied; +} + +bool DreamPicoPort::isSingleDevice() const { + return is_single_device; +} + +void DreamPicoPort::determineHardwareBus(int joystick_idx, SDL_Joystick* sdl_joystick) { + // This function determines what bus index to use when communicating with the hardware. +#if defined(_WIN32) + // This only works in Windows because the joystick_path is not given in other OSes + const char* joystick_name = SDL_JoystickName(sdl_joystick); + const char* joystick_path = SDL_JoystickPath(sdl_joystick); + + struct SDL_hid_device_info* devs = SDL_hid_enumerate(VID, PID); + if (devs) { + if (!devs->next) { + // Only single device found, so this is simple (host-1p firmware used) + hardware_bus = 0; + is_hardware_bus_implied = false; + is_single_device = true; + } else { + struct SDL_hid_device_info* it = devs; + struct SDL_hid_device_info* my_dev = nullptr; + + if (joystick_path) + { + while (it) + { + // Note: hex characters will be differing case, so case-insensitive cmp is needed + if (it->path && 0 == SDL_strcasecmp(it->path, joystick_path)) { + my_dev = it; + break; + } + it = it->next; + } + } + + if (my_dev) { + it = devs; + int count = 0; + if (my_dev->serial_number) { + while (it) { + if (it->serial_number && + 0 == wcscmp(it->serial_number, my_dev->serial_number)) + { + ++count; + } + it = it->next; + } + + if (count == 1) { + // Single device of this serial found + is_single_device = true; + hardware_bus = 0; + is_hardware_bus_implied = false; + } else { + is_single_device = false; + if (my_dev->release_number < 0x0102) { + // Interfaces go in decending order + hardware_bus = (count - (my_dev->interface_number % 4) - 1); + is_hardware_bus_implied = false; + } else { + // Version 1.02 of interface will make interfaces in ascending order + hardware_bus = (my_dev->interface_number % 4); + is_hardware_bus_implied = false; + } + } + } + } + } + SDL_hid_free_enumeration(devs); + } +#endif + + if (hardware_bus < 0) { + // The number of buttons gives a clue as to what index the controller is + int nbuttons = SDL_JoystickNumButtons(sdl_joystick); + + if (nbuttons >= 32 || nbuttons <= 27) { + // Older version of firmware or single player + hardware_bus = 0; + is_hardware_bus_implied = true; + is_single_device = true; + } + else { + hardware_bus = 31 - nbuttons; + is_hardware_bus_implied = false; + is_single_device = false; + } + } +} + +bool DreamPicoPort::queryInterfaceVersion() { + std::string buffer; + asio::error_code error = serial->sendCmd("XV\n", buffer, timeout_ms); + if (error) { + WARN_LOG(INPUT, "DreamPicoPort[%d] send(XV) failed: %s", software_bus, error.message().c_str()); + return false; + } + + if (0 == strncmp("*failed", buffer.c_str(), 7) || 0 == strncmp("0: failed", buffer.c_str(), 9)) { + // Using a version of firmware before "XV" was available + interface_version = 0.0; + } + else { + try { + interface_version = std::stod(buffer); + } + catch(const std::exception&) { + WARN_LOG(INPUT, "DreamPicoPort[%d] command XV received invalid response: %s", software_bus, buffer.c_str()); + return false; + } + } + + return true; +} + +bool DreamPicoPort::queryPeripherals() { + peripherals.clear(); + expansionDevs = 0; + + MapleMsg msg; + msg.command = MDCF_GetCondition; + msg.destAP = (hardware_bus << 6) | 0x20; + msg.originAP = hardware_bus << 6; + msg.setData(MFID_0_Input); + + asio::error_code error = serial->sendMsg(msg, hardware_bus, msg, timeout_ms); + if (error) + { + WARN_LOG(INPUT, "DreamPicoPort[%d] send(condition) failed: %s", software_bus, error.message().c_str()); + return true; // assume simply controller not connected yet + } + + expansionDevs = msg.originAP & 0x1f; + + if (interface_version >= 1.0) { + // Can just use X? + std::string buffer; + error = serial->sendCmd("X?" + std::to_string(hardware_bus) + "\n", buffer, timeout_ms); + if (error) { + WARN_LOG(INPUT, "DreamPicoPort[%d] send(X?) failed: %s", software_bus, error.message().c_str()); + return false; + } + + { + std::istringstream stream(buffer); + std::string outerGroup; + while (std::getline(stream, outerGroup, ';')) { + if (outerGroup.empty() || outerGroup == ",") continue; + std::vector> outerList; + std::istringstream outerStream(outerGroup.substr(1)); // Skip the leading '{' + std::string innerGroup; + + while (std::getline(outerStream, innerGroup, '}')) { + if (innerGroup.empty() || innerGroup == ",") continue; + std::array innerList = {{0, 0}}; + std::istringstream innerStream(innerGroup.substr(1)); // Skip the leading '{' + std::string number; + std::size_t idx = 0; + + while (std::getline(innerStream, number, ',')) { + if (!number.empty() && number[0] == '{') { + number = number.substr(1); + } + uint32_t value; + std::stringstream ss; + ss << std::hex << number; + ss >> value; + if (idx < 2) { + innerList[idx] = value; + } + ++idx; + } + + outerList.push_back(innerList); + } + + peripherals.push_back(outerList); + } + } + } + else { + // TODO: probably should just pop up a toast asking user to update firmware + // Manually query each sub-peripheral + peripherals.push_back({}); // skip controller since it's not used + for (u32 i = 0; i < 2; ++i) { + std::vector> portPeripherals; + u8 port = (1 << i); + if (expansionDevs & port) { + msg.command = MDC_DeviceRequest; + msg.destAP = (hardware_bus << 6) | port; + msg.originAP = hardware_bus << 6; + msg.size = 0; + + error = serial->sendMsg(msg, hardware_bus, msg, timeout_ms); + if (error) { + WARN_LOG(INPUT, "DreamPicoPort[%d] send(query) failed: %s", software_bus, error.message().c_str()); + return false; + } + + if (msg.size < 4) { + WARN_LOG(INPUT, "DreamPicoPort[%d] read(query) failed: invalid size %d", software_bus, msg.size); + return false; + } + + const u32 fnCode = (msg.data[0] << 24) | (msg.data[1] << 16) | (msg.data[2] << 8) | msg.data[3]; + u8 fnIdx = 1; + u32 mask = 0x80000000; + while (mask > 0) { + if (fnCode & mask) { + u32 i = fnIdx++ * 4; + u32 code = (msg.data[i] << 24) | (msg.data[i+1] << 16) | (msg.data[i+2] << 8) | msg.data[i+3]; + std::array peripheral = {{mask, code}}; + portPeripherals.push_back(std::move(peripheral)); + } + mask >>= 1; + } + + } + peripherals.push_back(portPeripherals); + } + } + + return true; +} + +#endif // USE_DREAMCASTCONTROLLER diff --git a/core/sdl/dreampicoport.h b/core/sdl/dreampicoport.h new file mode 100644 index 000000000..1aaae531b --- /dev/null +++ b/core/sdl/dreampicoport.h @@ -0,0 +1,110 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once + +#include "dreamlink.h" + +#ifdef USE_DREAMCASTCONTROLLER + +#include + +#include +#include +#include +#include + +// Forward declaration of underlying serial connection +class DreamPicoPortSerialHandler; + +//! See: https://github.com/OrangeFox86/DreamPicoPort +class DreamPicoPort : public DreamLink +{ + u8 expansionDevs = 0; + + //! The one and only serial port + static std::unique_ptr serial; + //! Number of devices using the above serial + static std::atomic connected_dev_count; + //! Current timeout in milliseconds + std::chrono::milliseconds timeout_ms; + //! The bus ID dictated by flycast + int software_bus = -1; + //! The bus index of the hardware connection which will differ from the software bus + int hardware_bus = -1; + //! true iff only a single devices was found when enumerating devices + bool is_single_device = true; + //! True when initial enumeration failed + bool is_hardware_bus_implied = true; + //! True once connection is established + bool connection_established = false; + //! The queried interface version + double interface_version = 0.0; + //! The queried peripherals; for each function, index 0 is function code and index 1 is the function definition + std::vector>> peripherals; + +public: + //! Dreamcast Controller USB VID:1209 PID:2f07 + static constexpr const std::uint16_t VID = 0x1209; + static constexpr const std::uint16_t PID = 0x2f07; + static constexpr const char* VID_PID_GUID = "09120000072f0000"; + +public: + DreamPicoPort(int bus, int joystick_idx, SDL_Joystick* sdl_joystick); + + virtual ~DreamPicoPort(); + + bool send(const MapleMsg& msg) override; + + bool send(const MapleMsg& txMsg, MapleMsg& rxMsg) override; + + void gameTermination() override; + + int getBus() const override; + + u32 getFunctionCode(int forPort) const override; + + int getDefaultBus() const override; + + void changeBus(int newBus); + + std::string getName() const override; + + void connect() override; + + void disconnect() override; + + void sendPort(); + + int hardwareBus() const; + + bool isHardwareBusImplied() const; + + bool isSingleDevice() const; + +private: + asio::error_code sendCmd(const std::string& cmd); + asio::error_code sendMsg(const MapleMsg& msg); + asio::error_code receiveCmd(std::string& cmd); + asio::error_code receiveMsg(MapleMsg& msg); + void determineHardwareBus(int joystick_idx, SDL_Joystick* sdl_joystick); + bool queryInterfaceVersion(); + bool queryPeripherals(); +}; + +#endif // USE_DREAMCASTCONTROLLER diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index a6f84cfcd..f25529269 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -30,7 +30,7 @@ #include "nswitch.h" #include "switch_gamepad.h" #endif -#include "dreamconn.h" +#include "dreamlink.h" #include static SDL_Window* window = NULL; @@ -83,8 +83,8 @@ static void sdl_open_joystick(int index) std::shared_ptr gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); #else std::shared_ptr gamepad; - if (DreamConnGamepad::isDreamcastController(index)) - gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); + if (DreamLinkGamepad::isDreamcastController(index)) + gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); else gamepad = std::make_shared(index < MAPLE_PORTS ? index : -1, index, pJoystick); #endif diff --git a/core/ui/gui.cpp b/core/ui/gui.cpp index f9f162509..9895e1616 100644 --- a/core/ui/gui.cpp +++ b/core/ui/gui.cpp @@ -54,6 +54,7 @@ #include "hw/pvr/Renderer_if.h" #if defined(USE_SDL) #include "sdl/sdl.h" +#include "sdl/dreamlink.h" #endif #include "vgamepad.h" @@ -434,7 +435,7 @@ static void gui_newFrame() io.AddKeyEvent(ImGuiKey_GamepadDpadRight, ((kcode[0] & DC_DPAD_RIGHT) == 0)); io.AddKeyEvent(ImGuiKey_GamepadDpadUp, ((kcode[0] & DC_DPAD_UP) == 0)); io.AddKeyEvent(ImGuiKey_GamepadDpadDown, ((kcode[0] & DC_DPAD_DOWN) == 0)); - + float analog; analog = joyx[0] < 0 ? -(float)joyx[0] / 32768.f : 0.f; io.AddKeyAnalogEvent(ImGuiKey_GamepadLStickLeft, analog > 0.1f, analog); @@ -820,8 +821,8 @@ const char *maple_device_types[] = // "Dreameye", }; -const char *maple_expansion_device_types[] = -{ +const char *maple_expansion_device_types[] = +{ "None", "Sega VMU", "Vibration Pack", @@ -2032,6 +2033,10 @@ static void gui_settings_controls(bool& maple_devices_changed) #if defined(_WIN32) && !defined(TARGET_UWP) OptionCheckbox("Use Raw Input", config::UseRawInput, "Supports multiple pointing devices (mice, light guns) and keyboards"); #endif +#ifdef USE_DREAMCASTCONTROLLER + OptionCheckbox("Use Physical VMU Memory", config::UsePhysicalVmuMemory, + "Enables direct read/write access to physical VMU memory via DreamPicoPort/DreamConn."); +#endif ImGui::Spacing(); header("Dreamcast Devices"); @@ -2848,7 +2853,6 @@ static void gui_settings_advanced() } OptionCheckbox("Dump Textures", config::DumpTextures, "Dump all textures into data/texdump/"); - bool logToFile = cfgLoadBool("log", "LogToFile", false); if (ImGui::Checkbox("Log to File", &logToFile)) cfgSaveBool("log", "LogToFile", logToFile); @@ -3268,7 +3272,7 @@ static void gui_display_content() const int itemsPerLine = std::max(totalWidth / (uiScaled(150) + ImGui::GetStyle().ItemSpacing.x), 1); const float responsiveBoxSize = totalWidth / itemsPerLine - ImGui::GetStyle().FramePadding.x * 2; const ImVec2 responsiveBoxVec2 = ImVec2(responsiveBoxSize, responsiveBoxSize); - + if (config::BoxartDisplayMode) ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f, 0.5f)); else @@ -3737,7 +3741,7 @@ void gui_display_profiler() ImGui::Unindent(); } } - + for (const fc_profiler::ProfileThread* profileThread : fc_profiler::ProfileThread::s_allThreads) { fc_profiler::drawGraph(*profileThread);