diff --git a/README.md b/README.md index 65c31574..3c10a324 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,9 @@ you can set its DNS address from within the emulated console's Wi-Fi settings menu. > [!NOTE] -> Do not confuse this with local multiplayer. -> melonDS DS does not support emulating local wireless -> at this time. +> Do not confuse this with local multiplayer, +> which does not require access to the Internet +> and is implemented using libretro's netplay API. ## Homebrew Save Data @@ -191,11 +191,6 @@ These features have not yet been implemented in standalone [melonDS][melonds], or they haven't been integrated into melonDS DS. If you want to see them, ask how you can get involved! -- **Local Wireless:** - Upstream melonDS supports emulating local wireless multiplayer - (e.g. Multi-Card Play, Download Play) with multiple instances of melonDS on the same computer - or on the same network. - This feature is not yet integrated into melonDS DS. - **Homebrew Savestates:** melonDS has limited support for taking savestates of homebrew games, as the virtual SD card is not included in savestate data. diff --git a/src/libretro/CMakeLists.txt b/src/libretro/CMakeLists.txt index cfe70cb6..7f1e7d38 100644 --- a/src/libretro/CMakeLists.txt +++ b/src/libretro/CMakeLists.txt @@ -60,6 +60,8 @@ add_library(melondsds_libretro ${LIBRARY_TYPE} net/pcap.hpp net/net.cpp net/net.hpp + net/mp.cpp + net/mp.hpp platform/file.cpp platform/lan.cpp platform/mp.cpp diff --git a/src/libretro/core/core.cpp b/src/libretro/core/core.cpp index 4b0c46be..57126455 100644 --- a/src/libretro/core/core.cpp +++ b/src/libretro/core/core.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include diff --git a/src/libretro/core/core.hpp b/src/libretro/core/core.hpp index 339f5acc..59052d77 100644 --- a/src/libretro/core/core.hpp +++ b/src/libretro/core/core.hpp @@ -18,6 +18,7 @@ #define MELONDSDS_CORE_HPP #include +#include #include #include @@ -33,6 +34,7 @@ #include "../PlatformOGLPrivate.h" #include "../sram.hpp" #include "net/net.hpp" +#include "net/mp.hpp" #include "std/span.hpp" struct retro_game_info; @@ -92,6 +94,14 @@ namespace MelonDsDs { int LanSendPacket(std::span data) noexcept; int LanRecvPacket(uint8_t* data) noexcept; + void MpStarted(retro_netpacket_send_t send, retro_netpacket_poll_receive_t poll_receive) noexcept; + void MpPacketReceived(const void *buf, size_t len, uint16_t client_id) noexcept; + void MpStopped() noexcept; + bool MpSendPacket(const Packet &p) noexcept; + std::optional MpNextPacket() noexcept; + std::optional MpNextPacketBlock() noexcept; + bool MpActive() const noexcept; + void WriteNdsSave(std::span savedata, uint32_t writeoffset, uint32_t writelen) noexcept; void WriteGbaSave(std::span savedata, uint32_t writeoffset, uint32_t writelen) noexcept; void WriteFirmware(const melonDS::Firmware& firmware, uint32_t writeoffset, uint32_t writelen) noexcept; @@ -142,6 +152,7 @@ namespace MelonDsDs { InputState _inputState {}; MicrophoneState _micState {}; RenderStateWrapper _renderState {}; + MpState _mpState {}; std::optional _ndsInfo = std::nullopt; std::optional _gbaInfo = std::nullopt; std::optional _gbaSaveInfo = std::nullopt; diff --git a/src/libretro/environment.cpp b/src/libretro/environment.cpp index 4e2ee71d..81a3b275 100644 --- a/src/libretro/environment.cpp +++ b/src/libretro/environment.cpp @@ -39,6 +39,7 @@ #include "libretro.hpp" #include "config/config.hpp" #include "core/test.hpp" +#include "net/mp.hpp" #include "tracy.hpp" #include "version.hpp" @@ -737,6 +738,13 @@ PUBLIC_SYMBOL void retro_set_environment(retro_environment_t cb) { retro_core_options_update_display_callback update_display_cb {MelonDsDs::UpdateOptionVisibility}; environment(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK, &update_display_cb); + retro_netpacket_callback netpacket_callback { + .start = &MelonDsDs::MpStarted, + .receive = &MelonDsDs::MpReceived, + .stop = &MelonDsDs::MpStopped, + }; + environment(RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE, &netpacket_callback); + environment(RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE, (void*) MelonDsDs::content_overrides); environment(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*) MelonDsDs::ports); @@ -869,4 +877,4 @@ PUBLIC_SYMBOL void retro_set_input_poll(retro_input_poll_t input_poll) { PUBLIC_SYMBOL void retro_set_input_state(retro_input_state_t input_state) { retro::_input_state = input_state; -} \ No newline at end of file +} diff --git a/src/libretro/libretro.cpp b/src/libretro/libretro.cpp index 2429a520..16ec9e34 100644 --- a/src/libretro/libretro.cpp +++ b/src/libretro/libretro.cpp @@ -57,7 +57,6 @@ using std::unique_ptr; using std::make_unique; using retro::task::TaskSpec; - namespace MelonDsDs { // Aligned with CoreState to prevent undefined behavior alignas(CoreState) static std::array CoreStateBuffer; @@ -325,4 +324,84 @@ void Platform::WriteFirmware(const Firmware& firmware, u32 writeoffset, u32 writ ZoneScopedN(TracyFunction); MelonDsDs::Core.WriteFirmware(firmware, writeoffset, writelen); -} \ No newline at end of file +} + +extern "C" void MelonDsDs::MpStarted(uint16_t client_id, retro_netpacket_send_t send_fn, retro_netpacket_poll_receive_t poll_receive_fn) noexcept { + MelonDsDs::Core.MpStarted(send_fn, poll_receive_fn); +} + +extern "C" void MelonDsDs::MpReceived(const void* buf, size_t len, uint16_t client_id) noexcept { + MelonDsDs::Core.MpPacketReceived(buf, len, client_id); +} + +extern "C" void MelonDsDs::MpStopped() noexcept { + MelonDsDs::Core.MpStopped(); +} + +int DeconstructPacket(u8 *data, u64 *timestamp, const std::optional &o_p) { + if (!o_p.has_value()) { + return 0; + } + memcpy(data, o_p->Data(), o_p->Length()); + *timestamp = o_p->Timestamp(); + return o_p->Length(); +} + +int Platform::MP_SendPacket(u8* data, int len, u64 timestamp, void*) { + return MelonDsDs::Core.MpSendPacket(MelonDsDs::Packet(data, len, timestamp, 0, MelonDsDs::Packet::Type::Other)) ? len : 0; +} + +int Platform::MP_RecvPacket(u8* data, u64* timestamp, void*) { + std::optional o_p = MelonDsDs::Core.MpNextPacket(); + return DeconstructPacket(data, timestamp, o_p); +} + +int Platform::MP_SendCmd(u8* data, int len, u64 timestamp, void*) { + return MelonDsDs::Core.MpSendPacket(MelonDsDs::Packet(data, len, timestamp, 0, MelonDsDs::Packet::Type::Cmd)) ? len : 0; +} + +int Platform::MP_SendReply(u8 *data, int len, u64 timestamp, u16 aid, void*) { + // aid is always less than 16, + // otherwise sending a 16-bit wide aidmask in RecvReplies wouldn't make sense, + // and neither would this line[1] from melonDS itself. + // A blog post from melonDS[2] from 2017 also confirms that + // "each client is given an ID from 1 to 15" + // [1] https://github.com/melonDS-emu/melonDS/blob/817b409ec893fb0b2b745ee18feced08706419de/src/net/LAN.cpp#L1074 + // [2] https://melonds.kuribo64.net/comments.php?id=25 + retro_assert(aid < 16); + return MelonDsDs::Core.MpSendPacket(MelonDsDs::Packet(data, len, timestamp, aid, MelonDsDs::Packet::Type::Reply)) ? len : 0; +} + +int Platform::MP_SendAck(u8* data, int len, u64 timestamp, void*) { + return MelonDsDs::Core.MpSendPacket(MelonDsDs::Packet(data, len, timestamp, 0, MelonDsDs::Packet::Type::Cmd)) ? len : 0; +} + +int Platform::MP_RecvHostPacket(u8* data, u64 * timestamp, void*) { + std::optional o_p = MelonDsDs::Core.MpNextPacketBlock(); + return DeconstructPacket(data, timestamp, o_p); +} + +u16 Platform::MP_RecvReplies(u8* packets, u64 timestamp, u16 aidmask, void*) { + if(!MelonDsDs::Core.MpActive()) { + return 0; + } + u16 ret = 0; + int loops = 0; + while((ret & aidmask) != aidmask) { + std::optional o_p = MelonDsDs::Core.MpNextPacketBlock(); + if(!o_p.has_value()) { + return ret; + } + MelonDsDs::Packet p = std::move(o_p).value(); + if(p.Timestamp() < (timestamp - 32)) { + continue; + } + if(p.PacketType() != MelonDsDs::Packet::Type::Reply) { + continue; + } + ret |= 1< +#include +#include +#include +using namespace MelonDsDs; + +constexpr long RECV_TIMEOUT_MS = 25; + +uint64_t swapToNetwork(uint64_t n) { + return swap_if_little64(n); +} + +Packet Packet::parsePk(const void *buf, uint64_t len) { + // Necessary because arithmetic on void* is forbidden + const char *indexableBuf = (const char *)buf; + const char *data = indexableBuf + HeaderSize; + retro_assert(len >= HeaderSize); + size_t dataLen = len - HeaderSize; + uint64_t timestamp = swapToNetwork(*(const uint64_t*)(indexableBuf)); + uint8_t aid = *(const uint8_t*)(indexableBuf + 8); + uint8_t type = *(const uint8_t*)(indexableBuf + 9); + // type 2 means cmd frame + // type 1 means reply frame + // type 0 means anything else + retro_assert(type == 2 || type == 1 || type == 0); + Packet::Type pkType; + switch (type) { + case 0: + pkType = Other; + break; + case 1: + pkType = Reply; + break; + case 2: + pkType = Cmd; + break; + } + return Packet(data, dataLen, timestamp, aid, pkType); +} + +Packet::Packet(const void *data, uint64_t len, uint64_t timestamp, uint8_t aid, Packet::Type type) : + _data((unsigned char*)data, (unsigned char*)data + len), + _timestamp(timestamp), + _aid(aid), + _type(type){ +} + +std::vector Packet::ToBuf() const { + std::vector ret; + ret.reserve(HeaderSize + Length()); + uint64_t netTimestamp = swapToNetwork(_timestamp); + ret.insert(ret.end(), (const char *)&netTimestamp, ((const char *)&netTimestamp) + sizeof(uint64_t)); + ret.push_back(_aid); + uint8_t numericalType = 0; + switch(_type) { + case Other: + numericalType = 0; + break; + case Reply: + numericalType = 1; + break; + case Cmd: + numericalType = 2; + break; + } + ret.push_back(numericalType); + ret.insert(ret.end(), _data.begin(), _data.end()); + return ret; +} + +bool MpState::IsReady() const noexcept { + return _sendFn != nullptr && _pollFn != nullptr; +} + +void MpState::SetSendFn(retro_netpacket_send_t sendFn) noexcept { + _sendFn = sendFn; +} + +void MpState::SetPollFn(retro_netpacket_poll_receive_t pollFn) noexcept { + _pollFn = pollFn; +} + +void MpState::PacketReceived(const void *buf, size_t len, uint16_t client_id) noexcept { + retro_assert(IsReady()); + Packet p = Packet::parsePk(buf, len); + if(p.PacketType() == Packet::Type::Cmd) { + _hostId = client_id; + //retro::debug("Host client id is {}", client_id); + } + receivedPackets.push(std::move(p)); +} + +std::optional MpState::NextPacket() noexcept { + retro_assert(IsReady()); + if(receivedPackets.empty()) { + _sendFn(RETRO_NETPACKET_FLUSH_HINT, NULL, 0, RETRO_NETPACKET_BROADCAST); + _pollFn(); + } + if(receivedPackets.empty()) { + return std::nullopt; + } else { + Packet p = receivedPackets.front(); + receivedPackets.pop(); + return p; + } +} + +std::optional MpState::NextPacketBlock() noexcept { + retro_assert(IsReady()); + if (receivedPackets.empty()) { + for(std::clock_t start = std::clock(); std::clock() < (start + (RECV_TIMEOUT_MS * CLOCKS_PER_SEC / 1000));) { + _sendFn(RETRO_NETPACKET_FLUSH_HINT, NULL, 0, RETRO_NETPACKET_BROADCAST); + _pollFn(); + if(!receivedPackets.empty()) { + return NextPacket(); + } + } + } else { + return NextPacket(); + } + retro::debug("Timeout while waiting for packet"); + return std::nullopt; +} + +void MpState::SendPacket(const Packet &p) noexcept { + retro_assert(IsReady()); + uint16_t dest = RETRO_NETPACKET_BROADCAST; + if(p.PacketType() == Packet::Type::Cmd) { + _hostId = std::nullopt; + } + if(p.PacketType() == Packet::Type::Reply && _hostId.has_value()) { + dest = _hostId.value(); + } + _sendFn(RETRO_NETPACKET_UNSEQUENCED | RETRO_NETPACKET_UNRELIABLE | RETRO_NETPACKET_FLUSH_HINT, p.ToBuf().data(), p.Length() + HeaderSize, dest); +} + + diff --git a/src/libretro/net/mp.hpp b/src/libretro/net/mp.hpp new file mode 100644 index 00000000..a263e3f9 --- /dev/null +++ b/src/libretro/net/mp.hpp @@ -0,0 +1,60 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace MelonDsDs { +// timestamp, aid, and isReply, respectively. +constexpr size_t HeaderSize = sizeof(uint64_t) + sizeof(uint8_t) + sizeof(uint8_t); + +class Packet { +public: + enum Type { + Reply, Cmd, Other + }; + + static Packet parsePk(const void *buf, uint64_t len); + explicit Packet(const void *data, uint64_t len, uint64_t timestamp, uint8_t aid, Packet::Type type); + + [[nodiscard]] uint64_t Timestamp() const noexcept { + return _timestamp; + }; + [[nodiscard]] uint8_t Aid() const noexcept { + return _aid; + }; + [[nodiscard]] Packet::Type PacketType() const noexcept { + return _type; + } + [[nodiscard]] const void *Data() const noexcept { + return _data.data(); + }; + [[nodiscard]] uint64_t Length() const noexcept { + return _data.size(); + }; + + std::vector ToBuf() const; +private: + uint64_t _timestamp; + uint8_t _aid; + Packet::Type _type; + std::vector _data; +}; + +class MpState { +public: + void PacketReceived(const void *buf, size_t len, uint16_t client_id) noexcept; + void SetSendFn(retro_netpacket_send_t sendFn) noexcept; + void SetPollFn(retro_netpacket_poll_receive_t pollFn) noexcept; + bool IsReady() const noexcept; + void SendPacket(const Packet &p) noexcept; + std::optional NextPacket() noexcept; + std::optional NextPacketBlock() noexcept; +private: + retro_netpacket_send_t _sendFn; + retro_netpacket_poll_receive_t _pollFn; + std::optional _hostId; + std::queue receivedPackets; +}; +} diff --git a/src/libretro/platform/mp.cpp b/src/libretro/platform/mp.cpp index ee187e8b..c77ed658 100644 --- a/src/libretro/platform/mp.cpp +++ b/src/libretro/platform/mp.cpp @@ -15,41 +15,69 @@ */ #include - -//! Local multiplayer is not implemented in melonDS DS. +#include "tracy.hpp" +#include "core/core.hpp" +#include "environment.hpp" +#include +#include using namespace melonDS; -void Platform::MP_Begin(void*) { +void MelonDsDs::CoreState::MpStarted(retro_netpacket_send_t send, retro_netpacket_poll_receive_t poll_receive) noexcept { + ZoneScopedN(TracyFunction); + _mpState.SetSendFn(send); + _mpState.SetPollFn(poll_receive); + retro::info("Starting multiplayer on libretro side"); } -void Platform::MP_End(void*) { +void MelonDsDs::CoreState::MpPacketReceived(const void *buf, size_t len, uint16_t client_id) noexcept { + ZoneScopedN(TracyFunction); + _mpState.PacketReceived(buf, len, client_id); } -int Platform::MP_SendPacket(u8*, int, u64, void*) { - return 0; +void MelonDsDs::CoreState::MpStopped() noexcept { + ZoneScopedN(TracyFunction); + _mpState.SetSendFn(nullptr); + _mpState.SetPollFn(nullptr); + retro::info("Stopping multiplayer on libretro side"); } -int Platform::MP_RecvPacket(u8*, u64*, void*) { - return 0; +bool MelonDsDs::CoreState::MpSendPacket(const MelonDsDs::Packet &p) noexcept { + ZoneScopedN(TracyFunction); + if(!_mpState.IsReady()) { + return false; + } + _mpState.SendPacket(p); + return true; } -int Platform::MP_SendCmd(u8*, int, u64, void*) { - return 0; +std::optional MelonDsDs::CoreState::MpNextPacket() noexcept { + ZoneScopedN(TracyFunction); + if(!_mpState.IsReady()) { + return std::nullopt; + } + return _mpState.NextPacket(); } -int Platform::MP_SendReply(u8*, int, u64, u16, void*) { - return 0; +std::optional MelonDsDs::CoreState::MpNextPacketBlock() noexcept { + ZoneScopedN(TracyFunction); + if(!_mpState.IsReady()) { + return std::nullopt; + } + return _mpState.NextPacketBlock(); } -int Platform::MP_SendAck(u8*, int, u64, void*) { - return 0; +bool MelonDsDs::CoreState::MpActive() const noexcept { + return _mpState.IsReady(); } -int Platform::MP_RecvHostPacket(u8*, u64 *, void*) { - return 0; +// Not much we can do in Begin and End +void Platform::MP_Begin(void*) { + ZoneScopedN(TracyFunction); + retro::info("Starting multiplayer on DS side"); } -u16 Platform::MP_RecvReplies(u8*, u64, u16, void*) { - return 0; -} \ No newline at end of file +void Platform::MP_End(void*) { + ZoneScopedN(TracyFunction); + retro::info("Ending multiplayer on DS side"); +}