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 (2 players max) #242

Merged
merged 24 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
545871b
Drop stale packets. Makes it harder to establish connections
BernardoGomesNegri Dec 15, 2024
d7227f2
Revert "Drop stale packets. Makes it harder to establish connections"
BernardoGomesNegri Dec 15, 2024
754ec47
Prepare for future >2 players support
BernardoGomesNegri Dec 26, 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, 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 @@ -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, 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
BernardoGomesNegri marked this conversation as resolved.
Show resolved Hide resolved
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
Loading