Commit ab163e85 authored by Jack Andersen's avatar Jack Andersen

Initial commit

parents
cmake_minimum_required(VERSION 3.0)
project(jbus)
include_directories(include)
add_library(jbus
include/jbus/optional.hpp
include/jbus/Socket.hpp
src/Common.cpp include/jbus/Common.hpp
src/Endpoint.cpp include/jbus/Endpoint.hpp
src/Listener.cpp include/jbus/Listener.hpp)
add_executable(joyboot tools/joyboot.cpp)
target_link_libraries(joyboot jbus)
## JBus
This is a library for communicating with emulated GameBoy Advance instances
using the JoyBus protocol, linked over TCP.
Currently, only [VBA-M](https://github.com/visualboyadvance-m/visualboyadvance-m)
is known to function. It uses the same networking method as the
[Dolphin](https://github.com/dolphin-emu/dolphin) GameCube emulator.
#ifndef JBUS_COMMON_HPP
#define JBUS_COMMON_HPP
#include <functional>
#include <stdint.h>
#include <stdlib.h>
namespace jbus
{
using s8 = int8_t;
using u8 = uint8_t;
using s16 = int16_t;
using u16 = uint16_t;
using s32 = int32_t;
using u32 = uint32_t;
using s64 = int64_t;
using u64 = uint64_t;
#undef bswap16
#undef bswap32
#undef bswap64
/* Type-sensitive byte swappers */
template <typename T>
static inline T bswap16(T val)
{
#if __GNUC__
return __builtin_bswap16(val);
#elif _WIN32
return _byteswap_ushort(val);
#else
return (val = (val << 8) | ((val >> 8) & 0xFF));
#endif
}
template <typename T>
static inline T bswap32(T val)
{
#if __GNUC__
return __builtin_bswap32(val);
#elif _WIN32
return _byteswap_ulong(val);
#else
val = (val & 0x0000FFFF) << 16 | (val & 0xFFFF0000) >> 16;
val = (val & 0x00FF00FF) << 8 | (val & 0xFF00FF00) >> 8;
return val;
#endif
}
template <typename T>
static inline T bswap64(T val)
{
#if __GNUC__
return __builtin_bswap64(val);
#elif _WIN32
return _byteswap_uint64(val);
#else
return ((val & 0xFF00000000000000ULL) >> 56) |
((val & 0x00FF000000000000ULL) >> 40) |
((val & 0x0000FF0000000000ULL) >> 24) |
((val & 0x000000FF00000000ULL) >> 8) |
((val & 0x00000000FF000000ULL) << 8) |
((val & 0x0000000000FF0000ULL) << 24) |
((val & 0x000000000000FF00ULL) << 40) |
((val & 0x00000000000000FFULL) << 56);
#endif
}
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
static inline int16_t SBig(int16_t val) {return bswap16(val);}
static inline uint16_t SBig(uint16_t val) {return bswap16(val);}
static inline int32_t SBig(int32_t val) {return bswap32(val);}
static inline uint32_t SBig(uint32_t val) {return bswap32(val);}
static inline int64_t SBig(int64_t val) {return bswap64(val);}
static inline uint64_t SBig(uint64_t val) {return bswap64(val);}
static inline float SBig(float val)
{
int32_t ival = bswap32(*((int32_t*)(&val)));
return *((float*)(&ival));
}
static inline double SBig(double val)
{
int64_t ival = bswap64(*((int64_t*)(&val)));
return *((double*)(&ival));
}
#ifndef SBIG
#define SBIG(q) ( ( (q) & 0x000000FF ) << 24 | ( (q) & 0x0000FF00 ) << 8 \
| ( (q) & 0x00FF0000 ) >> 8 | ( (q) & 0xFF000000 ) >> 24 )
#endif
static inline int16_t SLittle(int16_t val) {return val;}
static inline uint16_t SLittle(uint16_t val) {return val;}
static inline int32_t SLittle(int32_t val) {return val;}
static inline uint32_t SLittle(uint32_t val) {return val;}
static inline int64_t SLittle(int64_t val) {return val;}
static inline uint64_t SLittle(uint64_t val) {return val;}
static inline float SLittle(float val) {return val;}
static inline double SLittle(double val) {return val;}
#ifndef SLITTLE
#define SLITTLE(q) (q)
#endif
#else
static inline int16_t SLittle(int16_t val) {return bswap16(val);}
static inline uint16_t SLittle(uint16_t val) {return bswap16(val);}
static inline int32_t SLittle(int32_t val) {return bswap32(val);}
static inline uint32_t SLittle(uint32_t val) {return bswap32(val);}
static inline int64_t SLittle(int64_t val) {return bswap64(val);}
static inline uint64_t SLittle(uint64_t val) {return bswap64(val);}
static inline float SLittle(float val)
{
int32_t ival = bswap32(*((int32_t*)(&val)));
return *((float*)(&ival));
}
static inline double SLittle(double val)
{
int64_t ival = bswap64(*((int64_t*)(&val)));
return *((double*)(&ival));
}
#ifndef SLITTLE
#define SLITTLE(q) ( ( (q) & 0x000000FF ) << 24 | ( (q) & 0x0000FF00 ) << 8 \
| ( (q) & 0x00FF0000 ) >> 8 | ( (q) & 0xFF000000 ) >> 24 )
#endif
static inline int16_t SBig(int16_t val) {return val;}
static inline uint16_t SBig(uint16_t val) {return val;}
static inline int32_t SBig(int32_t val) {return val;}
static inline uint32_t SBig(uint32_t val) {return val;}
static inline int64_t SBig(int64_t val) {return val;}
static inline uint64_t SBig(uint64_t val) {return val;}
static inline float SBig(float val) {return val;}
static inline double SBig(double val) {return val;}
#ifndef SBIG
#define SBIG(q) (q)
#endif
#endif
class Endpoint;
class EndpointLocal;
enum EJStatFlags
{
GBA_JSTAT_MASK = 0x3a,
GBA_JSTAT_FLAGS_SHIFT = 4,
GBA_JSTAT_FLAGS_MASK = 0x30,
GBA_JSTAT_PSF1 = 0x20,
GBA_JSTAT_PSF0 = 0x10,
GBA_JSTAT_SEND = 0x08,
GBA_JSTAT_RECV = 0x02
};
enum EJoyReturn
{
GBA_READY = 0,
GBA_NOT_READY = 1,
GBA_BUSY = 2,
GBA_JOYBOOT_UNKNOWN_STATE = 3,
GBA_JOYBOOT_ERR_INVALID = 4
};
using FGBACallback = std::function<void(EndpointLocal& endpoint, EJoyReturn status)>;
u64 GetGCTicks();
void WaitGCTicks(u64 ticks);
static constexpr u64 GetGCTicksPerSec() { return 486000000ull; }
void Initialize();
}
#endif // JBUS_COMMON_HPP
#ifndef JBUS_ENDPOINT_HPP
#define JBUS_ENDPOINT_HPP
#include "Common.hpp"
#include "Socket.hpp"
#include "optional.hpp"
#include <thread>
namespace jbus
{
/** Self-contained class for solving Kawasedo's GBA BootROM challenge.
* GBA will boot client_pad.bin code on completion. */
class KawasedoChallenge
{
/** DSP-hosted public-key unwrap and initial message crypt
* Reference: https://github.com/dolphin-emu/dolphin/blob/master/Source/Core/Core/HW/DSPHLE/UCodes/GBA.cpp */
struct DSPSecParms
{
/* Nonce challenge (first read from GBA, hence already little-endian) */
u32 x0_gbaChallenge;
/* Palette of pulsing logo on GBA during transmission [0,6] */
u32 x4_logoPalette;
/* Speed and direction of palette interpolation [-4,4] */
u32 x8_logoSpeed;
/* Length of JoyBoot program to upload */
u32 xc_progLength;
/* Unwrapped public key */
u32 x20_publicKey;
/* Message authentication code */
u32 x24_authInitCode;
void ProcessGBACrypto()
{
/* Unwrap key from challenge using 'sedo' magic number (to encrypt JoyBoot program) */
x20_publicKey = x0_gbaChallenge ^ 0x6f646573;
/* Pack palette parameters */
u16 paletteSpeedCoded;
s16 logoSpeed = static_cast<s8>(x8_logoSpeed);
if (logoSpeed < 0)
paletteSpeedCoded = ((-logoSpeed + 2) * 2) | (x4_logoPalette << 4);
else if (logoSpeed == 0)
paletteSpeedCoded = (x4_logoPalette * 2) | 0x70;
else /* logo_speed > 0 */
paletteSpeedCoded = ((logoSpeed - 1) * 2) | (x4_logoPalette << 4);
/* JoyBoot ROMs start with a padded header; this is the length beyond that header */
s32 lengthNoHeader = ROUND_UP_8(xc_progLength) - 0x200;
/* The JoyBus protocol transmits in 4-byte packets while flipping a state flag;
* so the GBA BIOS counts the program length in 8-byte packet-pairs */
u16 packetPairCount = (lengthNoHeader < 0) ? 0 : lengthNoHeader / 8;
paletteSpeedCoded |= (packetPairCount & 0x4000) >> 14;
/* Pack together encoded transmission parameters */
u32 t1 = (((packetPairCount << 16) | 0x3f80) & 0x3f80ffff) * 2;
t1 += (static_cast<s16>(static_cast<s8>(t1 >> 8)) & packetPairCount) << 16;
u32 t2 = ((paletteSpeedCoded & 0xff) << 16) + (t1 & 0xff0000) + ((t1 >> 8) & 0xffff00);
u32 t3 = paletteSpeedCoded << 16 | ((t2 << 8) & 0xff000000) | (t1 >> 16) | 0x80808080;
/* Wrap with 'Kawa' or 'sedo' (Kawasedo is the author of the BIOS cipher) */
x24_authInitCode = t3 ^ ((t3 & 0x200) != 0 ? 0x6f646573 : 0x6177614b);
}
} xf8_dspHmac;
s32 x0_pColor;
s32 x4_pSpeed;
u8* x8_progPtr;
u32 xc_progLen;
u8* x10_statusPtr;
FGBACallback x14_callback;
u8 x18_readBuf[4];
u8 x1c_writeBuf[4];
s32 x20_byteInWindow;
u64 x28_ticksAfterXf;
u32 x30_justStarted;
u32 x34_bytesSent;
u32 x38_crc;
u32 x3c_checkStore[7];
s32 x58_currentKey;
s32 x5c_initMessage;
s32 x60_gameId;
u32 x64_totalBytes;
bool m_started = true;
void F23(EndpointLocal& endpoint, EJoyReturn status);
void F25(EndpointLocal& endpoint, EJoyReturn status);
void F27(EndpointLocal& endpoint, EJoyReturn status);
void F29(EndpointLocal& endpoint, EJoyReturn status);
void GBAX02();
void GBAX01(EndpointLocal& endpoint);
void F31(EndpointLocal& endpoint, EJoyReturn status);
void F33(EndpointLocal& endpoint, EJoyReturn status);
void F35(EndpointLocal& endpoint, EJoyReturn status);
void F37(EndpointLocal& endpoint, EJoyReturn status);
void F39(EndpointLocal& endpoint, EJoyReturn status);
auto bindThis(void(KawasedoChallenge::*ptmf)(EndpointLocal&, EJoyReturn))
{
return std::bind(ptmf, this, std::placeholders::_1, std::placeholders::_2);
}
public:
KawasedoChallenge(Endpoint& endpoint, s32 paletteColor, s32 paletteSpeed,
u8* programp, s32 length, u8* status, FGBACallback&& callback);
bool started() const { return m_started; }
u8 percentComplete() const
{
if (!x64_totalBytes)
return 0;
return x34_bytesSent * 100 / x64_totalBytes;
}
bool isDone() const { return !x14_callback; }
};
class Endpoint
{
friend class EndpointLocal;
enum EJoybusCmds
{
CMD_RESET = 0xff,
CMD_STATUS = 0x00,
CMD_READ = 0x14,
CMD_WRITE = 0x15
};
enum class EWaitResp
{
NoWait = 0,
WaitCmd,
WaitIdle
};
static const u64 BITS_PER_SECOND = 115200;
static const u64 BYTES_PER_SECOND = BITS_PER_SECOND / 8;
net::Socket m_dataSocket;
net::Socket m_clockSocket;
std::thread m_transferThread;
std::mutex m_syncLock;
std::condition_variable m_syncCv;
std::experimental::optional<KawasedoChallenge> m_joyBoot;
FGBACallback m_callback;
EWaitResp m_waitingResp = EWaitResp::NoWait;
size_t m_dataReceivedBytes = 0;
u8 m_buffer[5];
u64 m_timeSent = 0;
u8* m_readDstPtr = nullptr;
u8* m_statusPtr = nullptr;
u64 m_lastGCTick = 0;
u64 m_timeCmdSent = 0;
u8 m_lastCmd = 0;
u8 m_chan;
bool m_booted = false;
bool m_cmdIssued = false;
bool m_running = true;
static u64 getTransferTime(u8 cmd);
void clockSync();
void send(const u8* buffer);
size_t seceive(u8* buffer);
size_t runBuffer(u8* buffer, u64& remTicks, EWaitResp resp);
bool idleGetStatus(u64& remTicks);
void transferProc();
void transferWakeup(EndpointLocal& endpoint, u8 status);
auto bindSync()
{
return std::bind(&Endpoint::transferWakeup, this,
std::placeholders::_1, std::placeholders::_2);
}
public:
void stop();
EJoyReturn GBAGetProcessStatus(u8* percentp);
EJoyReturn GBAGetStatusAsync(u8* status, FGBACallback&& callback);
EJoyReturn GBAGetStatus(u8* status);
EJoyReturn GBAResetAsync(u8* status, FGBACallback&& callback);
EJoyReturn GBAReset(u8* status);
EJoyReturn GBAReadAsync(u8* dst, u8* status, FGBACallback&& callback);
EJoyReturn GBARead(u8* dst, u8* status);
EJoyReturn GBAWriteAsync(const u8* src, u8* status, FGBACallback&& callback);
EJoyReturn GBAWrite(const u8* src, u8* status);
EJoyReturn GBAJoyBootAsync(s32 paletteColor, s32 paletteSpeed,
u8* programp, s32 length, u8* status,
FGBACallback&& callback);
int GetChan() const { return m_chan; }
Endpoint(u8 chan, net::Socket&& data, net::Socket&& clock);
~Endpoint();
};
class EndpointLocal
{
friend class Endpoint;
Endpoint& m_ep;
EndpointLocal(Endpoint& ep) : m_ep(ep) {}
public:
EJoyReturn GBAGetStatusAsync(u8* status, FGBACallback&& callback);
EJoyReturn GBAResetAsync(u8* status, FGBACallback&& callback);
EJoyReturn GBAReadAsync(u8* dst, u8* status, FGBACallback&& callback);
EJoyReturn GBAWriteAsync(const u8* src, u8* status, FGBACallback&& callback);
int GetChan() const { return m_ep.GetChan(); }
};
}
#endif // JBUS_ENDPOINT_HPP
#ifndef JBUS_LISTENER_HPP
#define JBUS_LISTENER_HPP
#include "Common.hpp"
#include "Socket.hpp"
#include <thread>
#include <queue>
namespace jbus
{
class Listener
{
net::Socket m_dataServer = {false};
net::Socket m_clockServer = {false};
std::thread m_listenerThread;
std::mutex m_queueLock;
std::queue<std::unique_ptr<Endpoint>> m_endpointQueue;
bool m_running = false;
static const uint32_t DataPort = 0xd6ba;
static const uint32_t ClockPort = 0xc10c;
void listenerProc();
public:
void start();
void stop();
std::unique_ptr<Endpoint> accept();
~Listener();
};
}
#endif // JBUS_LISTENER_HPP
#ifndef JBUS_SOCKET_HPP
#define JBUS_SOCKET_HPP
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <fcntl.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>
#include <arpa/inet.h>
#include <string>
#include "Common.hpp"
namespace jbus
{
namespace net
{
/* Define the low-level send/receive flags, which depend on the OS */
#ifdef __linux__
static const int _flags = MSG_NOSIGNAL;
#else
static const int _flags = 0;
#endif
/** IP address class derived from SFML */
class IPAddress
{
uint32_t m_address = 0;
bool m_valid = false;
void resolve(const std::string& address)
{
m_address = 0;
m_valid = false;
if (address == "255.255.255.255")
{
/* The broadcast address needs to be handled explicitly,
* because it is also the value returned by inet_addr on error */
m_address = INADDR_BROADCAST;
m_valid = true;
}
else if (address == "0.0.0.0")
{
m_address = INADDR_ANY;
m_valid = true;
}
else
{
/* Try to convert the address as a byte representation ("xxx.xxx.xxx.xxx") */
uint32_t ip = inet_addr(address.c_str());
if (ip != INADDR_NONE)
{
m_address = ip;
m_valid = true;
}
else
{
/* Not a valid address, try to convert it as a host name */
addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
addrinfo* result = NULL;
if (getaddrinfo(address.c_str(), NULL, &hints, &result) == 0)
{
if (result)
{
ip = reinterpret_cast<sockaddr_in*>(result->ai_addr)->sin_addr.s_addr;
freeaddrinfo(result);
m_address = ip;
m_valid = true;
}
}
}
}
}
public:
IPAddress(const std::string& address)
{
resolve(address);
}
uint32_t toInteger() const
{
return ntohl(m_address);
}
operator bool() const { return m_valid; }
};
/** Server-oriented TCP socket class derived from SFML */
class Socket
{
int m_socket = -1;
bool m_isBlocking;
static sockaddr_in createAddress(uint32_t address, unsigned short port)
{
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_addr.s_addr = htonl(address);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
#ifdef __APPLE__
addr.sin_len = sizeof(addr);
#endif
return addr;
}
bool openSocket()
{
if (isOpen())
return false;
m_socket = socket(PF_INET, SOCK_STREAM, 0);
if (m_socket == -1)
{
fprintf(stderr, "Can't allocate socket");
return false;
}
int one = 1;
setsockopt(m_socket, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char*>(&one), sizeof(one));
#ifdef __APPLE__
setsockopt(m_socket, SOL_SOCKET, SO_NOSIGPIPE, reinterpret_cast<char*>(&one), sizeof(one));
#endif
setBlocking(m_isBlocking);
return true;
}
void setRemoteSocket(int remSocket)
{
close();
m_socket = remSocket;
setBlocking(m_isBlocking);
}
public:
enum class EResult
{
OK,
Error,
Busy
};
Socket(bool blocking)
: m_isBlocking(blocking) {}
~Socket() { close(); }
Socket(const Socket& other) = delete;
Socket& operator=(const Socket& other) = delete;
Socket(Socket&& other)
: m_socket(other.m_socket), m_isBlocking(other.m_isBlocking)
{
other.m_socket = -1;
}
Socket& operator=(Socket&& other)
{
close();
m_socket = other.m_socket;
other.m_socket = -1;
m_isBlocking = other.m_isBlocking;
return *this;
}
void setBlocking(bool blocking)
{
m_isBlocking = blocking;
int status = fcntl(m_socket, F_GETFL);
if (m_isBlocking)
fcntl(m_socket, F_SETFL, status & ~O_NONBLOCK);
else
fcntl(m_socket, F_SETFL, status | O_NONBLOCK);
}
bool isOpen() const { return m_socket != -1; }
bool openAndListen(const IPAddress& address, uint32_t port)
{
if (!openSocket())
return false;
sockaddr_in addr = createAddress(address.toInteger(), port);
if (bind(m_socket, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == -1)
{
/* Not likely to happen, but... */
fprintf(stderr, "Failed to bind listener socket to port %d", port);
return false;
}
if (::listen(m_socket, 0) == -1)
{
/* Oops, socket is deaf */
fprintf(stderr, "Failed to listen to port %d", port);
return false;
}
return true;
}
EResult accept(Socket& remoteSocketOut, sockaddr_in& fromAddress)
{
if (!isOpen())
return EResult::Error;
/* Accept a new connection */
socklen_t length = sizeof(sockaddr_in);
int remoteSocket = ::accept(m_socket, reinterpret_cast<sockaddr*>(&fromAddress), &length);
/* Check for errors */
if (remoteSocket == -1)
{
EResult res = (errno == EAGAIN) ? EResult::Busy : EResult::Error;
if (res == EResult::Error)
fprintf(stderr, "Failed to accept incoming connection: %s", strerror(errno));
return res;
}
/* Initialize the new connected socket */
remoteSocketOut.setRemoteSocket(remoteSocket);
return EResult::OK;
}
EResult accept(Socket& remoteSocketOut)
{
sockaddr_in fromAddress;
return accept(remoteSocketOut, fromAddress);
}
EResult accept(Socket& remoteSocketOut, std::string& fromHostname)
{
sockaddr_in fromAddress;
socklen_t len = sizeof(fromAddress);
char name[NI_MAXHOST];
EResult res = accept(remoteSocketOut, fromAddress);
if (res == EResult::OK)
if (getnameinfo((sockaddr*)&fromAddress, len, name, NI_MAXHOST, nullptr, 0, 0) == 0)
fromHostname.assign(name);
return res;
}
void close()
{
if (!isOpen())
return;
::close(m_socket);
m_socket = -1;
}
EResult send(const void* buf, size_t len, size_t& transferred)
{
transferred = 0;
if (!isOpen())
return EResult::Error;
if (!buf || !len)
return EResult::Error;
/* Loop until every byte has been sent */
ssize_t result = 0;
for (size_t sent = 0; sent < len; sent += result)
{
/* Send a chunk of data */
result = ::send(m_socket, static_cast<const char*>(buf) + sent, len - sent, _flags);
/* Check for errors */
if (result < 0)
return (errno == EAGAIN) ? EResult::Busy : EResult::Error;
}
transferred = len;
return EResult::OK;
}
EResult send(const void* buf, size_t len)
{
size_t transferred;
return send(buf, len, transferred);
}