Skip to content

Commit

Permalink
Merge pull request #242 from BernardoGomesNegri/local-mp
Browse files Browse the repository at this point in the history
Emulated local Wi-Fi multiplayer support (2 players max)
  • Loading branch information
JesseTG authored Dec 26, 2024
2 parents 6126b1d + 754ec47 commit a3c57f0
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 31 deletions.
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -194,11 +194,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.
Expand Down
2 changes: 2 additions & 0 deletions src/libretro/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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
Expand Down
1 change: 0 additions & 1 deletion src/libretro/core/core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
#include <GPU3D_OpenGL.h>
#include <GPU3D_Soft.h>

#include <libretro.h>
#include <retro_assert.h>

#include <NDS.h>
Expand Down
11 changes: 11 additions & 0 deletions src/libretro/core/core.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#define MELONDSDS_CORE_HPP

#include <cstddef>
#include <libretro.h>
#include <memory>
#include <regex>

Expand All @@ -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;
Expand Down Expand Up @@ -89,6 +91,14 @@ namespace MelonDsDs {
int LanSendPacket(std::span<std::byte> 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<Packet> MpNextPacket() noexcept;
std::optional<Packet> MpNextPacketBlock() noexcept;
bool MpActive() const noexcept;

void WriteNdsSave(std::span<const std::byte> savedata, uint32_t writeoffset, uint32_t writelen) noexcept;
void WriteGbaSave(std::span<const std::byte> savedata, uint32_t writeoffset, uint32_t writelen) noexcept;
void WriteFirmware(const melonDS::Firmware& firmware, uint32_t writeoffset, uint32_t writelen) noexcept;
Expand Down Expand Up @@ -139,6 +149,7 @@ namespace MelonDsDs {
InputState _inputState {};
MicrophoneState _micState {};
RenderStateWrapper _renderState {};
MpState _mpState {};
std::optional<retro::GameInfo> _ndsInfo = std::nullopt;
std::optional<retro::GameInfo> _gbaInfo = std::nullopt;
std::optional<retro::GameInfo> _gbaSaveInfo = std::nullopt;
Expand Down
10 changes: 9 additions & 1 deletion src/libretro/environment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "libretro.hpp"
#include "config/config.hpp"
#include "core/test.hpp"
#include "net/mp.hpp"
#include "tracy.hpp"
#include "version.hpp"

Expand Down Expand Up @@ -778,6 +779,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);

Expand Down Expand Up @@ -914,4 +922,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;
}
}
83 changes: 81 additions & 2 deletions src/libretro/libretro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::byte, sizeof(CoreState)> CoreStateBuffer;
Expand Down Expand Up @@ -329,4 +328,84 @@ void Platform::WriteFirmware(const Firmware& firmware, u32 writeoffset, u32 writ
ZoneScopedN(TracyFunction);

MelonDsDs::Core.WriteFirmware(firmware, writeoffset, writelen);
}
}

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<MelonDsDs::Packet> &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<MelonDsDs::Packet> 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<MelonDsDs::Packet> 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<MelonDsDs::Packet> 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<<p.Aid();
memcpy(&packets[(p.Aid()-1)*1024], p.Data(), std::min(p.Length(), (uint64_t)1024));
loops++;
}
return ret;
}
3 changes: 3 additions & 0 deletions src/libretro/libretro.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ namespace MelonDsDs {
extern "C" void HardwareContextReset() noexcept;
extern "C" void HardwareContextDestroyed() noexcept;
extern "C" bool UpdateOptionVisibility() noexcept;
extern "C" void MpStarted(uint16_t client_id, retro_netpacket_send_t send_fn, retro_netpacket_poll_receive_t poll_receive_fn) noexcept;
extern "C" void MpReceived(const void* buf, size_t len, uint16_t client_id) noexcept;
extern "C" void MpStopped() noexcept;
}

#endif //MELONDS_DS_LIBRETRO_HPP
139 changes: 139 additions & 0 deletions src/libretro/net/mp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include "mp.hpp"
#include "environment.hpp"
#include <ctime>
#include <libretro.h>
#include <retro_assert.h>
#include <retro_endianness.h>
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<uint8_t> Packet::ToBuf() const {
std::vector<uint8_t> 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<Packet> 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<Packet> 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);
}


Loading

0 comments on commit a3c57f0

Please sign in to comment.