Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emulated local Wi-Fi multiplayer support #242

Open
wants to merge 21 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dfe22f2
Add prototype for local multiplayer
BernardoGomesNegri Nov 15, 2024
5e3e6ff
Fix whitespace
BernardoGomesNegri Nov 15, 2024
d802a1b
Fix multiplayer, still laggy and slow
BernardoGomesNegri Nov 16, 2024
ef20060
add timeout on recvreplies
BernardoGomesNegri Dec 1, 2024
fc68607
improve network code, better timeout handling
BernardoGomesNegri Dec 1, 2024
934d317
remove needless logging
BernardoGomesNegri Dec 1, 2024
b3efee8
remove cmake endianness detection, no longer needed
BernardoGomesNegri Dec 1, 2024
4449b45
remove unneeded functions
BernardoGomesNegri Dec 1, 2024
2d2c7fc
update README
BernardoGomesNegri Dec 1, 2024
bcb1cac
add pragma once to header
BernardoGomesNegri Dec 1, 2024
0e864c7
add const, noexcept and pass-by-ref according to @JesseTG 's suggestions
BernardoGomesNegri Dec 2, 2024
c48b034
remove unnecessary deinit
BernardoGomesNegri Dec 2, 2024
ae2dd3b
reduce copies following @JesseTG 's suggestion
BernardoGomesNegri Dec 2, 2024
63d9afb
mark functions called from C code as noexcept
BernardoGomesNegri Dec 2, 2024
1bee2c3
Add noexcept to extern c functions (in declaration and definition thi…
BernardoGomesNegri Dec 2, 2024
8a30907
add tracepoints
BernardoGomesNegri Dec 2, 2024
d2b3d54
remove unneeded function
BernardoGomesNegri Dec 3, 2024
bf8a008
add comment explaining assert
BernardoGomesNegri Dec 3, 2024
a70e2ba
further explain assert
BernardoGomesNegri Dec 3, 2024
0863649
fix big mistake
BernardoGomesNegri Dec 3, 2024
0dc9f33
Allow zero-sized packets
BernardoGomesNegri Dec 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/libretro/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -92,6 +94,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) noexcept;
void MpStopped() noexcept;
bool MpSendPacket(const Packet &p) const 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 @@ -142,6 +152,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 @@ -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"

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
}
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 @@ -325,4 +324,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);
}

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, false)) ? 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, false)) ? 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
BernardoGomesNegri marked this conversation as resolved.
Show resolved Hide resolved
retro_assert(aid < 16);
return MelonDsDs::Core.MpSendPacket(MelonDsDs::Packet(data, len, timestamp, aid, true)) ? len : 0;
}

int Platform::MP_SendAck(u8* data, int len, u64 timestamp, void*) {
return MelonDsDs::Core.MpSendPacket(MelonDsDs::Packet(data, len, timestamp, 0, false)) ? 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) {
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.IsReply()) {
continue;
}
ret |= 1<<p.Aid();
memcpy(&packets[(p.Aid()-1)*1024], p.Data(), p.Length());
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
100 changes: 100 additions & 0 deletions src/libretro/net/mp.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#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);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this assertion doesn't hold up; I just triggered it in Mario Kart DS. I created the in-game lobby from one of my devices, but as soon as the second device connected this retro_assert went off.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try changing it to len >= HeaderSize. If it works, then zero sized packets are possible.
Interestingly, I was not able to reproduce this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Managed to reproduce it, but switching to len >= HeaderSize fixes it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What game did you test it with? I used Mario Kart DS.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me too. .nds file format, MD5 hash: 8cbf438d5a7c0d9acbe56a9b627ca6c5
The game indeed crashes if the condition is len > HeaderSize but switching to len >= HeaderSize fixed it for me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially tested it with Pokémon HeartGold as well.

size_t dataLen = len - HeaderSize;
uint64_t timestamp = swapToNetwork(*(const uint64_t*)(indexableBuf));
uint8_t aid = *(const uint8_t*)(indexableBuf + 8);
uint8_t isReply = *(const uint8_t*)(indexableBuf + 9);
retro_assert(isReply == 1 || isReply == 0);
return Packet(data, dataLen, timestamp, aid, isReply == 1);
}

Packet::Packet(const void *data, uint64_t len, uint64_t timestamp, uint8_t aid, bool isReply) :
_data((unsigned char*)data, (unsigned char*)data + len),
_timestamp(timestamp),
_aid(aid),
_isReply(isReply){
}

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);
ret.push_back(_isReply);
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) noexcept {
retro_assert(IsReady());
receivedPackets.push(Packet::parsePk(buf, len));
}

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) const noexcept {
retro_assert(IsReady());
_sendFn(RETRO_NETPACKET_UNSEQUENCED | RETRO_NETPACKET_UNRELIABLE | RETRO_NETPACKET_FLUSH_HINT, p.ToBuf().data(), p.Length() + HeaderSize, RETRO_NETPACKET_BROADCAST);
}


55 changes: 55 additions & 0 deletions src/libretro/net/mp.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#pragma once
#include <cstdint>
#include <queue>
#include <optional>
#include <vector>
#include <libretro.h>

namespace MelonDsDs {
// timestamp, aid, and isReply, respectively.
constexpr size_t HeaderSize = sizeof(uint64_t) + sizeof(uint8_t) + sizeof(uint8_t);

class Packet {
public:
static Packet parsePk(const void *buf, uint64_t len);
explicit Packet(const void *data, uint64_t len, uint64_t timestamp, uint8_t aid, bool isReply);

[[nodiscard]] uint64_t Timestamp() const noexcept {
return _timestamp;
};
[[nodiscard]] uint8_t Aid() const noexcept {
return _aid;
};
[[nodiscard]] bool IsReply() const noexcept {
return _isReply;
};
[[nodiscard]] const void *Data() const noexcept {
return _data.data();
};
[[nodiscard]] uint64_t Length() const noexcept {
return _data.size();
};

std::vector<uint8_t> ToBuf() const;
private:
uint64_t _timestamp;
uint8_t _aid;
bool _isReply;
std::vector<uint8_t> _data;
};

class MpState {
public:
void PacketReceived(const void *buf, size_t len) 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) const noexcept;
std::optional<Packet> NextPacket() noexcept;
std::optional<Packet> NextPacketBlock() noexcept;
private:
retro_netpacket_send_t _sendFn;
retro_netpacket_poll_receive_t _pollFn;
std::queue<Packet> receivedPackets;
};
}
Loading
Loading