#ifndef LINK_WIRELESS_H #define LINK_WIRELESS_H // -------------------------------------------------------------------------- // A high level driver for the GBA Wireless Adapter. // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: // LinkWireless* linkWireless = new LinkWireless(); // - 2) Add the required interrupt service routines: (*) // interrupt_init(); // interrupt_add(INTR_VBLANK, LINK_WIRELESS_ISR_VBLANK); // interrupt_add(INTR_SERIAL, LINK_WIRELESS_ISR_SERIAL); // interrupt_add(INTR_TIMER3, LINK_WIRELESS_ISR_TIMER); // - 3) Initialize the library with: // linkWireless->activate(); // - 4) Start a server: // linkWireless->serve(); // // // `getState()` should return SERVING now... // // `currentPlayerId()` should return 0 // // `playerCount()` should return the number of active consoles // - 5) Connect to a server: // LinkWireless::Server servers[LINK_WIRELESS_MAX_SERVERS]; // u32 serverCount; // linkWireless->getServers(servers, serverCount); // if (serverCount == 0) return; // // linkWireless->connect(servers[0].id); // while (linkWireless->getState() == LinkWireless::State::CONNECTING) // linkWireless->keepConnecting(); // // // `getState()` should return CONNECTED now... // // `currentPlayerId()` should return 1, 2, 3, or 4 (the host is 0) // // `playerCount()` should return the number of active consoles // - 6) Send data: // linkWireless->send(0x1234); // - 7) Receive data: // LinkWireless::Message messages[LINK_WIRELESS_QUEUE_SIZE]; // u32 receivedCount; // linkWireless->receive(messages, receivedCount); // - 8) Disconnect: // linkWireless->activate(); // // (resets the adapter) // -------------------------------------------------------------------------- // (*) libtonc's interrupt handler sometimes ignores interrupts due to a bug. // That causes packet loss. You REALLY want to use libugba's instead. // (see examples) // -------------------------------------------------------------------------- #ifndef LINK_DEVELOPMENT #pragma GCC system_header #endif #include "_link_common.hpp" #include "LinkRawWireless.hpp" #ifndef LINK_WIRELESS_QUEUE_SIZE /** * @brief Buffer size (how many incoming and outgoing messages the queues can * store at max **per player**). The default value is `30`, which seems fine for * most games. * \warning This affects how much memory is allocated. With the default value, * it's around `480` bytes. There's a double-buffered incoming queue and a * double-buffered outgoing queue (to avoid data races). * \warning You can approximate the usage with `LINK_WIRELESS_QUEUE_SIZE * 16`. */ #define LINK_WIRELESS_QUEUE_SIZE 30 #endif #ifndef LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH /** * @brief Max server transfer length per timer tick. Must be in the range * `[6;21]`. The default value is `11`. Higher values will use the bandwidth * more efficiently but also consume more CPU! * \warning This is measured in words (1 message = 1 halfword). One word is used * as a header, so a max transfer length of 11 could transfer up to 20 messages. */ #define LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH 11 #endif #ifndef LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH /** * @brief Max client transfer length per timer tick. Must be in the range * `[2;4]`. The default value is `4`. Changing this is not recommended, it's * already too low. * \warning This is measured in words (1 message = 1 halfword). One halfword is * used as a header, so a max transfer length of 4 could transfer up to 7 * messages. */ #define LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH 4 #endif #ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM /** * @brief Put Interrupt Service Routines (ISR) in IWRAM (uncomment to enable). * This can significantly improve performance due to its faster access, but it's * disabled by default to conserve IWRAM space, which is limited. * \warning If you enable this, make sure that `lib/iwram_code/LinkWireless.cpp` * gets compiled! For example, in a Makefile-based project, verify that the * directory is in your `SRCDIRS` list. */ // #define LINK_WIRELESS_PUT_ISR_IN_IWRAM #endif #ifndef LINK_WIRELESS_ENABLE_NESTED_IRQ /** * @brief Allow LINK_WIRELESS_ISR_* functions to be interrupted (uncomment to * enable). * This can be useful, for example, if your audio engine requires calling a * VBlank handler with precise timing. */ // #define LINK_WIRELESS_ENABLE_NESTED_IRQ #endif // --- LINK_WIRELESS_PUT_ISR_IN_IWRAM knobs --- #ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL #define LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL 1 #endif #ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER #define LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER 1 #endif #ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL_LEVEL #define LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL_LEVEL "-Ofast" #endif #ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER_LEVEL #define LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER_LEVEL "-Ofast" #endif //--- LINK_VERSION_TAG LINK_WIRELESS_VERSION = "vLinkWireless/v8.0.3"; #define LINK_WIRELESS_MAX_PLAYERS LINK_RAW_WIRELESS_MAX_PLAYERS #define LINK_WIRELESS_MIN_PLAYERS 2 #define LINK_WIRELESS_MAX_SERVERS LINK_RAW_WIRELESS_MAX_SERVERS #define LINK_WIRELESS_MAX_GAME_ID 0x7FFF #define LINK_WIRELESS_MAX_GAME_NAME_LENGTH 14 #define LINK_WIRELESS_MAX_USER_NAME_LENGTH 8 #define LINK_WIRELESS_DEFAULT_TIMEOUT 10 #define LINK_WIRELESS_DEFAULT_INTERVAL 75 #define LINK_WIRELESS_DEFAULT_SEND_TIMER_ID 3 #define LINK_WIRELESS_RESET_IF_NEEDED \ if (!isEnabled) \ return false; \ if (linkRawWireless.getState() == State::NEEDS_RESET) \ if (!reset()) \ return false; #ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM #if LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL == 1 #define LINK_WIRELESS_SERIAL_ISR LINK_INLINE #else #define LINK_WIRELESS_SERIAL_ISR #endif #if LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER == 1 #define LINK_WIRELESS_TIMER_ISR LINK_INLINE #else #define LINK_WIRELESS_TIMER_ISR #endif #define LINK_WIRELESS_ISR_FUNC(name, params, args, body) \ void name params; \ LINK_INLINE void _##name params body #else #define LINK_WIRELESS_SERIAL_ISR #define LINK_WIRELESS_TIMER_ISR #define LINK_WIRELESS_ISR_FUNC(name, params, args, body) \ void name params { \ _##name args; \ } \ LINK_INLINE void _##name params body #endif /** * @brief A high level driver for the GBA Wireless Adapter. */ class LinkWireless { private: using u32 = Link::u32; using u16 = Link::u16; using u8 = Link::u8; using vu8 = Link::vu8; static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024; static constexpr int BROADCAST_SEARCH_WAIT_FRAMES = 60; static constexpr int MAX_PACKET_IDS_SERVER = 1 << 6; static constexpr int MAX_PACKET_IDS_CLIENT = 1 << 4; static constexpr int MAX_INFLIGHT_PACKETS_SERVER = MAX_PACKET_IDS_SERVER / 2 - 1; static constexpr int MAX_INFLIGHT_PACKETS_CLIENT = MAX_PACKET_IDS_CLIENT / 2 - 1; static constexpr int NO_ID_ASSIGNED_YET = 0xFF; static constexpr u32 NO_ACK_RECEIVED_YET = 0xFFFFFFFF; static constexpr int HAS_FIRST_MSG_MASK = 0b10000; static constexpr int MAX_PLAYER_BITMAP_ENTRIES = 5; static constexpr int PLAYER_ID_BITS = 3; static constexpr int PLAYER_ID_MASK = 0b111; static constexpr int BIT_HAS_MORE = 15; public: // #define LINK_WIRELESS_PROFILING_ENABLED #ifdef LINK_WIRELESS_PROFILING_ENABLED u32 vblankTime = 0; u32 serialTime = 0; u32 timerTime = 0; u32 vblankIRQs = 0; u32 serialIRQs = 0; u32 timerIRQs = 0; #endif using State = LinkRawWireless::State; using SignalLevelResponse = LinkRawWireless::SignalLevelResponse; enum class Error { // User errors NONE = 0, WRONG_STATE = 1, GAME_NAME_TOO_LONG = 2, USER_NAME_TOO_LONG = 3, BUFFER_IS_FULL = 4, // Communication errors COMMAND_FAILED = 5, CONNECTION_FAILED = 6, SEND_DATA_FAILED = 7, RECEIVE_DATA_FAILED = 8, ACKNOWLEDGE_FAILED = 9, TIMEOUT = 10, REMOTE_TIMEOUT = 11, BUSY_TRY_AGAIN = 12, }; struct Message { u16 data = 0; u8 playerId = 0; u8 packetId = NO_ID_ASSIGNED_YET; }; struct Server { u16 id = 0; u16 gameId; char gameName[LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1]; char userName[LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1]; u8 currentPlayerCount; bool isFull() { return currentPlayerCount == 0; } }; /** * @brief Constructs a new LinkWireless object. * @param forwarding If `true`, the server forwards all messages to the * clients. Otherwise, clients only see messages sent from the server * (ignoring other peers). * @param retransmission If `true`, the library handles retransmission for * you, so there should be no packet loss. * @param maxPlayers `(2~5)` Maximum number of allowed players. * @param timeout Number of *frames* without receiving *any* data to reset the * connection. * @param interval Number of *1024-cycle ticks* (61.04μs) between transfers * *(75 = 4.578ms)*. It's the interval of Timer #`sendTimerId`. Lower values * will transfer faster but also consume more CPU. * @param sendTimerId `(0~3)` GBA Timer to use for sending. * \warning You can use `Link::perFrame(...)` to convert from *packets per * frame* to *interval values*. */ explicit LinkWireless(bool forwarding = true, bool retransmission = true, u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS, u32 timeout = LINK_WIRELESS_DEFAULT_TIMEOUT, u16 interval = LINK_WIRELESS_DEFAULT_INTERVAL, u8 sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID) { config.forwarding = forwarding; config.retransmission = retransmission; config.maxPlayers = maxPlayers; config.timeout = timeout; config.interval = interval; config.sendTimerId = sendTimerId; } /** * @brief Returns whether the library is active or not. */ [[nodiscard]] bool isActive() { return isEnabled; } /** * @brief Activates the library. When an adapter is connected, it changes the * state to `AUTHENTICATED`. It can also be used to disconnect or reset the * adapter. */ bool activate() { LINK_READ_TAG(LINK_WIRELESS_VERSION); static_assert(LINK_WIRELESS_QUEUE_SIZE >= 1); static_assert(LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH >= 6 && LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH <= 21); static_assert(LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH >= 2 && LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH <= 4); LINK_BARRIER; isEnabled = false; LINK_BARRIER; lastError = Error::NONE; bool success = reset(); LINK_BARRIER; isEnabled = true; LINK_BARRIER; return success; } /** * @brief Restores the state from an existing connection on the Wireless * Adapter hardware. This is useful, for example, after a fresh launch of a * Multiboot game, to synchronize the library with the current state and * avoid a reconnection. Returns whether the restoration was successful. * On success, the state should be either `SERVING` or `CONNECTED`. * \warning This should be used as a replacement for `activate()`. */ bool restoreExistingConnection() { LINK_BARRIER; isEnabled = false; LINK_BARRIER; resetState(); stopTimer(); startTimer(); if (!linkRawWireless.restoreExistingConnection() || linkRawWireless.sessionState.playerCount > config.maxPlayers) { deactivate(); return false; } LINK_BARRIER; isEnabled = true; LINK_BARRIER; return true; } /** * @brief Puts the adapter into a low consumption mode and then deactivates * the library. It returns a boolean indicating whether the transition to low * consumption mode was successful. * @param turnOff Whether the library should put the adapter in the low * consumption mode or not before deactivation. Defaults to `true`. */ bool deactivate(bool turnOff = true) { bool success = true; if (turnOff) success = activate() && linkRawWireless.bye(); lastError = Error::NONE; isEnabled = false; resetState(); stop(); return success; } /** * @brief Starts broadcasting a server and changes the state to `SERVING`. You * can optionally provide data that games will be able to read. If the adapter * is already serving, this method only updates the broadcast data. * @param gameName Game name. Maximum `14` characters + null terminator. * @param userName User name. Maximum `8` characters + null terminator. * @param gameId `(0 ~ 0x7FFF)` Game ID. * \warning Updating broadcast data while serving can fail if the adapter is * busy. In that case, this will return `false` and `getLastError()` will be * `BUSY_TRY_AGAIN`. */ bool serve(const char* gameName = "", const char* userName = "", u16 gameId = LINK_WIRELESS_MAX_GAME_ID) { LINK_WIRELESS_RESET_IF_NEEDED if (linkRawWireless.getState() != State::AUTHENTICATED && linkRawWireless.getState() != State::SERVING) return badRequest(Error::WRONG_STATE); if (Link::strlen(gameName) > LINK_WIRELESS_MAX_GAME_NAME_LENGTH) return badRequest(Error::GAME_NAME_TOO_LONG); if (Link::strlen(userName) > LINK_WIRELESS_MAX_USER_NAME_LENGTH) return badRequest(Error::USER_NAME_TOO_LONG); isSendingSyncCommand = true; if (isAsyncCommandActive()) return badRequest(Error::BUSY_TRY_AGAIN); if (linkRawWireless.getState() != State::SERVING) { if (!setup(config.maxPlayers)) return abort(Error::COMMAND_FAILED); } bool success = linkRawWireless.broadcast(gameName, userName, gameId, false); if (linkRawWireless.getState() != State::SERVING) success = success && linkRawWireless.startHost(false); if (!success) return abort(Error::COMMAND_FAILED); LINK_BARRIER; isSendingSyncCommand = false; LINK_BARRIER; return true; } /** * @brief Closes the server while keeping the session active, to prevent new * users from joining the room. * \warning This action can fail if the adapter is busy. In that case, * this will return `false` and `getLastError()` will be `BUSY_TRY_AGAIN`. */ bool closeServer() { LINK_WIRELESS_RESET_IF_NEEDED if (linkRawWireless.getState() != State::SERVING || linkRawWireless.sessionState.isServerClosed) return badRequest(Error::WRONG_STATE); isSendingSyncCommand = true; if (isAsyncCommandActive()) return badRequest(Error::BUSY_TRY_AGAIN); LinkRawWireless::PollConnectionsResponse response; bool success = linkRawWireless.endHost(response); if (!success) return abort(Error::COMMAND_FAILED); LINK_BARRIER; isSendingSyncCommand = false; LINK_BARRIER; return true; } /** * @brief Retrieves the signal level of each player (0-255). For hosts, the * array will contain the signal level of each client in indexes 1-4. For * clients, it will only include the index corresponding to the * `currentPlayerId()`. * @param response A structure that will be filled with the signal levels. * \warning For clients, this action can fail if the adapter is busy. In that * case, this will return `false` and `getLastError()` will be * `BUSY_TRY_AGAIN`. For hosts, you already have this data, so it's free! */ bool getSignalLevel(SignalLevelResponse& response) { LINK_WIRELESS_RESET_IF_NEEDED if (!isSessionActive()) return badRequest(Error::WRONG_STATE); if (linkRawWireless.getState() == LinkRawWireless::State::SERVING) { for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++) response.signalLevels[i] = sessionState.signalLevel.level[i]; return true; } isSendingSyncCommand = true; if (isAsyncCommandActive()) return badRequest(Error::BUSY_TRY_AGAIN); bool success = linkRawWireless.getSignalLevel(response); if (!success) return abort(Error::COMMAND_FAILED); LINK_BARRIER; isSendingSyncCommand = false; LINK_BARRIER; return true; } /** * @brief Fills the `servers` array with all the currently broadcasting * servers. * @param servers The array to be filled with data. * @param serverCount The number to be filled with the number of found * servers. * \warning This action takes 1 second to complete. * \warning For an async version, see `getServersAsyncStart()`. */ bool getServers(Server servers[], u32& serverCount) { return getServers(servers, serverCount, []() {}); } /** * @brief Fills the `servers` array with all the currently broadcasting * servers. * @param servers The array to be filled with data. * @param serverCount The number to be filled with the number of found * servers. * @param onWait A function which will be invoked each time VBlank starts. * \warning This action takes 1 second to complete. * \warning For an async version, see `getServersAsyncStart()`. */ template bool getServers(Server servers[], u32& serverCount, F onWait) { serverCount = 0; if (!getServersAsyncStart()) return false; waitVBlanks(BROADCAST_SEARCH_WAIT_FRAMES, onWait); if (!getServersAsyncEnd(servers, serverCount)) return false; return true; } /** * @brief Starts looking for broadcasting servers and changes the state to * `SEARCHING`. After this, call `getServersAsyncEnd(...)` 1 second later. */ bool getServersAsyncStart() { LINK_WIRELESS_RESET_IF_NEEDED if (linkRawWireless.getState() != State::AUTHENTICATED) return badRequest(Error::WRONG_STATE); bool success = linkRawWireless.broadcastReadStart(); if (!success) return abort(Error::COMMAND_FAILED); return true; } /** * @brief Fills the `servers` array with all the currently broadcasting * servers. Changes the state to `AUTHENTICATED` again. * @param servers The array to be filled with data. * @param serverCount The number to be filled with the number of found * servers. */ bool getServersAsyncEnd(Server servers[], u32& serverCount) { serverCount = 0; LINK_WIRELESS_RESET_IF_NEEDED if (linkRawWireless.getState() != State::SEARCHING) return badRequest(Error::WRONG_STATE); LinkRawWireless::BroadcastReadPollResponse response; bool success1 = linkRawWireless.broadcastReadPoll(response); if (!success1) return abort(Error::COMMAND_FAILED); bool success2 = linkRawWireless.broadcastReadEnd(); if (!success2) return abort(Error::COMMAND_FAILED); auto foundServers = response.servers; for (u32 i = 0; i < response.serversSize; i++) { Server server; server.id = foundServers[i].id; server.gameId = foundServers[i].gameId; for (u32 j = 0; j < LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1; j++) server.gameName[j] = foundServers[i].gameName[j]; for (u32 j = 0; j < LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1; j++) server.userName[j] = foundServers[i].userName[j]; u8 nextClientNumber = foundServers[i].nextClientNumber; server.currentPlayerCount = nextClientNumber == 0xFF ? 0 : 1 + nextClientNumber; servers[i] = server; } serverCount = response.serversSize; return true; } /** * @brief Starts a connection with `serverId` and changes the state to * `CONNECTING`. * @param serverId Device ID of the server. */ bool connect(u16 serverId) { LINK_WIRELESS_RESET_IF_NEEDED if (linkRawWireless.getState() != State::AUTHENTICATED) return badRequest(Error::WRONG_STATE); bool success = linkRawWireless.connect(serverId); if (!success) return abort(Error::COMMAND_FAILED); return true; } /** * @brief When connecting, this needs to be called until the state is * `CONNECTED`. It assigns a player ID. Keep in mind that `isConnected()` and * `playerCount()` won't be updated until the first message from the server * arrives. */ bool keepConnecting() { LINK_WIRELESS_RESET_IF_NEEDED if (linkRawWireless.getState() != State::CONNECTING) return badRequest(Error::WRONG_STATE); LinkRawWireless::ConnectionStatus response; bool success1 = linkRawWireless.keepConnecting(response); if (!success1) return abort(Error::COMMAND_FAILED); if (response.phase == LinkRawWireless::ConnectionPhase::STILL_CONNECTING) return true; else if (response.phase == LinkRawWireless::ConnectionPhase::ERROR) return abort(Error::COMMAND_FAILED); auto success2 = linkRawWireless.finishConnection(); if (!success2) return abort(Error::COMMAND_FAILED); return true; } /** * @brief Returns whether a `send(...)` call would fail due to the queue being * full or not. */ bool canSend() { return !sessionState.newOutgoingMessages.isFull(); } /** * @brief Enqueues `data` to be sent to other nodes. * @param data The value to be sent. */ bool send(u16 data) { LINK_WIRELESS_RESET_IF_NEEDED if (!isSessionActive()) return badRequest(Error::WRONG_STATE); if (!canSend()) { lastError = Error::BUFFER_IS_FULL; return false; } Message message; message.playerId = linkRawWireless.sessionState.currentPlayerId; message.data = data; sessionState.newOutgoingMessages.syncPush(message); return true; } /** * @brief Fills the `messages` array with incoming messages. * @param messages The array to be filled with data. * @param receivedCount The number to be filled with the number of received * messages. */ bool receive(Message messages[], u32& receivedCount) { receivedCount = 0; if (!isSessionActive()) return false; LINK_BARRIER; sessionState.incomingMessages.startReading(); LINK_BARRIER; while (!sessionState.incomingMessages.isEmpty()) { auto message = sessionState.incomingMessages.pop(); if (message.playerId < LINK_WIRELESS_MAX_PLAYERS) { messages[receivedCount] = message; receivedCount++; } } LINK_BARRIER; sessionState.incomingMessages.stopReading(); LINK_BARRIER; return true; } /** * @brief Returns the current state. * @return One of the enum values from `State`. */ [[nodiscard]] State getState() { return linkRawWireless.getState(); } /** * @brief Returns `true` if the player count is higher than `1`. */ [[nodiscard]] bool isConnected() { return linkRawWireless.sessionState.playerCount > 1; } /** * @brief Returns `true` if the state is `SERVING` or `CONNECTED`. */ [[nodiscard]] bool isSessionActive() { return linkRawWireless.getState() == State::SERVING || linkRawWireless.getState() == State::CONNECTED; } /** * @brief Returns `true` if the server was closed with `closeServer()`. */ [[nodiscard]] bool isServerClosed() { return linkRawWireless.sessionState.isServerClosed; } /** * @brief Returns the number of connected players (`1~5`). */ [[nodiscard]] u8 playerCount() { return linkRawWireless.sessionState.playerCount; } /** * @brief Returns the current player ID (`0~4`). */ [[nodiscard]] u8 currentPlayerId() { return linkRawWireless.sessionState.currentPlayerId; } /** * @brief Returns whether the internal queue lost messages at some point due * to being full. This can happen if your queue size is too low, if you * receive too much data without calling `receive(...)` enough times, or if * excessive `receive(...)` calls prevent the ISR from copying data. After * this call, the overflow flag is cleared if `clear` is `true` (default * behavior). */ bool didQueueOverflow(bool clear = true) { bool overflowReceive = sessionState.newIncomingMessages.overflow; bool overflowForwardedMessage = sessionState.outgoingMessages.overflow; if (clear) { sessionState.newIncomingMessages.overflow = false; sessionState.outgoingMessages.overflow = false; } return overflowReceive || overflowForwardedMessage; } /** * @brief Resets other players' timeout count to `0`. * \warning Call this if you changed `config.timeout`. */ void resetTimeout() { if (!isEnabled) return; LINK_BARRIER; sessionState.isResetTimeoutPending = true; LINK_BARRIER; } /** * @brief Restarts the send timer without disconnecting. * \warning Call this if you changed `config.interval`. */ void resetTimer() { if (!isEnabled) return; stopTimer(); startTimer(); } /** * @brief If one of the other methods returns `false`, you can inspect this to * know the cause. After this call, the last error is cleared if `clear` is * `true` (default behavior). * @param clear Whether it should clear the error or not. */ Error getLastError(bool clear = true) { Error error = lastError; if (clear) lastError = Error::NONE; return error; } /** * @brief Returns the number of total outgoing messages. * \warning This is internal API! */ [[nodiscard]] u32 _getPendingCount() { return sessionState.outgoingMessages.size(); } /** * @brief Returns the number of inflight outgoing messages. * \warning This is internal API! */ [[nodiscard]] u32 _getInflightCount() { return sessionState.inflightCount; } /** * @brief Returns the number of forwarded outgoing messages. * \warning This is internal API! */ [[nodiscard]] u32 _getForwardedCount() { return sessionState.forwardedCount; } /** * @brief Returns the last packet ID. * \warning This is internal API! */ [[nodiscard]] u32 _lastPacketId() { return sessionState.lastPacketId; } /** * @brief Returns the last ACK received from player ID 1. * \warning This is internal API! */ [[nodiscard]] u32 _lastAckFromClient1() { return sessionState.lastAckFromClients[1]; } /** * @brief Returns the last packet ID received from player ID 1. * \warning This is internal API! */ [[nodiscard]] u32 _lastPacketIdFromClient1() { return sessionState.lastPacketIdFromClients[1]; } /** * @brief Returns the last ACK received from the server. * \warning This is internal API! */ [[nodiscard]] u32 _lastAckFromServer() { return sessionState.lastAckFromServer; } /** * @brief Returns the last packet ID received from the server. * \warning This is internal API! */ [[nodiscard]] u32 _lastPacketIdFromServer() { return sessionState.lastPacketIdFromServer; } /** * @brief Returns the next pending packet ID. * \warning This is internal API! */ [[nodiscard]] u32 _nextPendingPacketId() { return sessionState.outgoingMessages.isEmpty() ? 0 : sessionState.outgoingMessages.peek().packetId; } #ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING /** * @brief Sets a logger function. * \warning This is internal API! */ void _setLogger(LinkRawWireless::Logger logger) { linkRawWireless.logger = logger; } #endif /** * @brief This method is called by the VBLANK interrupt handler. * \warning This is internal API! */ #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ LINK_NOINLINE void _onVBlank() { #else void _onVBlank() { #endif if (!isEnabled) return; #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ if (interrupt) { pendingVBlank = true; return; } #endif #ifdef LINK_WIRELESS_PROFILING_ENABLED profileStart(); #endif if (!isSessionActive()) return; if (sessionState.isResetTimeoutPending) { sessionState.recvTimeout = 0; for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++) sessionState.msgTimeouts[i] = 0; sessionState.isResetTimeoutPending = false; } if (isConnected() && !sessionState.recvFlag) sessionState.recvTimeout++; if (sessionState.recvTimeout >= config.timeout) return (void)abort(Error::TIMEOUT); if (!checkRemoteTimeouts()) return (void)abort(Error::REMOTE_TIMEOUT); sessionState.recvFlag = false; sessionState.signalLevelCalled = false; #ifdef LINK_WIRELESS_PROFILING_ENABLED vblankTime += profileStop(); vblankIRQs++; #endif } /** * @brief This method is called by the SERIAL interrupt handler. * \warning This is internal API! */ LINK_WIRELESS_ISR_FUNC(_onSerial, (), (), { #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ interrupt = true; LINK_BARRIER; // (nested interrupts are enabled by LinkRawWireless::_onSerial(...)) #endif ___onSerial(); #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ irqEnd(); #endif }) /** * @brief This method is called by the TIMER interrupt handler. * \warning This is internal API! */ LINK_WIRELESS_ISR_FUNC(_onTimer, (), (), { #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ if (interrupt) return; interrupt = true; LINK_BARRIER; Link::_REG_IME = 1; #endif ___onTimer(); #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ irqEnd(); #endif }) struct Config { bool forwarding; bool retransmission; u8 maxPlayers; u32 timeout; // can be changed in realtime, but call `resetTimeout()` u16 interval; // can be changed in realtime, but call `resetTimer()` u8 sendTimerId; }; /** * @brief LinkWireless configuration. * \warning `deactivate()` first, change the config, and `activate()` again! */ Config config; /** * Returns whether the current multiboot ROM has started wirelessly or not. */ static bool isWirelessMultibootRom() { const vu8* ewram = (const vu8*)0x02000000; static const u8 header[12] = {0x52, 0x46, 0x55, 0x2D, 0x4D, 0x42, 0x4F, 0x4F, 0x54, 0x00, 0x00, 0x00}; for (unsigned i = 0; i < 12; ++i) if (ewram[4 + i] != header[i]) return false; return true; } #ifndef LINK_WIRELESS_DEBUG_MODE private: #endif using MessageQueue = Link::Queue; struct SignalLevel { vu8 level[LINK_WIRELESS_MAX_PLAYERS] = {}; }; struct SessionState { MessageQueue incomingMessages; // read by user, write by irq&user MessageQueue outgoingMessages; // read and write by irq MessageQueue newIncomingMessages; // read and write by irq MessageQueue newOutgoingMessages; // read by irq, write by user&irq SignalLevel signalLevel; // write by irq, read by any u32 recvTimeout = 0; // (~= LinkCable::IRQTimeout) u32 msgTimeouts[LINK_WIRELESS_MAX_PLAYERS]; // (~= LinkCable::msgTimeouts) bool recvFlag = false; // (~= LinkCable::IRQFlag) bool msgFlags[LINK_WIRELESS_MAX_PLAYERS]; // (~= LinkCable::msgFlags) bool signalLevelCalled = false; bool sendReceiveLatch = false; // true = send ; false = receive bool shouldWaitForServer = false; bool didReceiveFirstPacketFromServer = false; u32 inflightCount = 0; u32 forwardedCount = 0; u32 lastPacketId = 0; u32 lastPacketIdFromServer = 0; u32 lastAckFromServer = 0; u32 lastPacketIdFromClients[LINK_WIRELESS_MAX_PLAYERS]; u32 lastAckFromClients[LINK_WIRELESS_MAX_PLAYERS]; int lastHeartbeatFromClients[LINK_WIRELESS_MAX_PLAYERS]; int localHeartbeat = -1; volatile bool isResetTimeoutPending = false; }; struct TransferHeader { // - This header is appended as the first word of every transfer. // - Packets ("messages") are 16-bit. // - Packet IDs are 0~63 (server) and 0~15 (clients). // - They wrap around, so 31 and 7 are the maximum number of inflight // packets as per the N/2-1 rule. // - Messages are sent in order and retransmitted until their ACK. e.g.: // * >> 1, 2, 3 // * >> 1, 2, 3, 4, 5 // * << ack=3 // * >> 4, 5, 6 // This wastes bandwidth but reduces latency, since waiting for a // retransmission until not receiving an ACK takes time, and games usually // care more about latency than bandwidth. // - The first message can be in the header itself (bits 0~15) when: // * (there *is* something to send) && (it's from a client) // * -> this is indicated with a 1 in `firstPacketId`'s bit 4 // - Each of the next words in the transfer contain two 16-bit messages. // * Low part first, high part last // * The last word can contain 1 or 2 messages depending on `hasLastMsg` // - When `forwarding` is enabled, the server can forward messages from // other clients. If the stream includes forwarded messages, this header // contains `hasPlayerBitMap`=1, and the next halfword is a // `PlayerBitMap`. unsigned int _reserved_ : 4; // unused (or first msg!) unsigned int ack4 : 4; // server: player 4 ACK (or first msg!) unsigned int ack3 : 4; // server: player 3 ACK (or first msg!) unsigned int ack2 : 4; // server: player 2 ACK (or first msg!) unsigned int ack1 : 6; // server: player 1 ACK // clients: server ACK unsigned int hasLastMsg : 1; // there's a msg in last word's high part unsigned int hasPlayerBitMap : 1; // server: next halfword is a PlayerBitMap unsigned int firstPacketId : 6; // next packets are assumed consecutive // clients only use 4 bits here! // `hasFirstMsg` is an imaginary flag // living in `firstPacketId`'s bit 4 unsigned int playerCount : 2; // server: playerCount (0 = 2; ...; 3 = 5) // clients: heartbeat (0~3) }; struct PlayerBitMap { // - This halfword is appended after the `TransferHeader` if the stream // contains messages from multiple users (when `forwarding` is enabled). // - `playerIds` is a bit-packed array of 5 3-bit entries, representing the // owners of the next 5 messages (bits 0~2 = first message's player ID). // - `hasMore` indicates whether there are more messages owned by player IDs // greater than 0, which adds another `PlayerBitMap` after the messages. // - Each `PlayerBitMap` occupies the space of a regular message, but // doesn't affect the packet ID sequence. By design, they are always // placed in the low part. // --- // Example: Let's say there's a stream with 12 packets (p1~pC). // Before pB, some of the packets are forwarded from other clients. // So, the transfer header (THD) takes the first word and sets // `hasPlayerBitMap=1`. The low part of the next word is a `PlayerBitMap` // (PB) and describes the owners of the following 5 messages (p1~p5). Since // there are more forwarded messages (last PB had `hasMore`=1), a new PB is // added describing p6~pA. The last two messages (pB~pC) are server // messages, so there's no need for a new PB (last PB had `hasMore`=0). // w00 w01 w02 w03 w04 w05 w06 w07 // |-----||--|--||--|--||--|--||--|--||--|--||--|--||--|--| // | THD ||p1|PB||p3|p2||p5|p4||p6|PB||p8|p7||pA|p9||pC|pB| // |-----||--|--||--|--||--|--||--|--||--|--||--|--||--|--| // --- unsigned int playerIds : 15; // 5 entries, 3 bits per player unsigned int hasMore : 1; // if true, there's another `PlayerBitMap` after // the next 5 messages }; template union U32Packer { H asStruct; u32 asInt; }; using CommandResult = LinkRawWireless::CommandResult; LinkRawWireless linkRawWireless; SessionState sessionState; u32 nextAsyncCommandData[LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH]; u32 nextAsyncCommandDataSize = 0; volatile bool isSendingSyncCommand = false; volatile Error lastError = Error::NONE; volatile bool isEnabled = false; #ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ volatile bool interrupt = false, pendingVBlank = false; LINK_INLINE void irqEnd() { Link::_REG_IME = 0; interrupt = false; LINK_BARRIER; if (pendingVBlank) { _onVBlank(); pendingVBlank = false; } } #endif LINK_INLINE void ___onSerial() { if (!isEnabled) return; #ifdef LINK_WIRELESS_PROFILING_ENABLED profileStart(); #endif int status = linkRawWireless._onSerial(false); if (status <= -4) { return (void)abort(Error::ACKNOWLEDGE_FAILED); } else if (status > 0) { auto result = linkRawWireless._getAsyncCommandResultRef(); processAsyncCommand(result); } #ifdef LINK_WIRELESS_PROFILING_ENABLED serialTime += profileStop(); serialIRQs++; #endif } LINK_INLINE void ___onTimer() { if (!isEnabled) return; #ifdef LINK_WIRELESS_PROFILING_ENABLED profileStart(); #endif if (!isSessionActive()) return; if (!isAsyncCommandActive()) checkConnectionsOrTransferData(); #ifdef LINK_WIRELESS_PROFILING_ENABLED timerTime += profileStop(); timerIRQs++; #endif } LINK_INLINE void processAsyncCommand( const LinkRawWireless::CommandResult* commandResult) { // (irq only) if (!commandResult->success) { return (void)abort( commandResult->commandId == LinkRawWireless::COMMAND_SEND_DATA ? Error::SEND_DATA_FAILED : commandResult->commandId == LinkRawWireless::COMMAND_RECEIVE_DATA ? Error::RECEIVE_DATA_FAILED : Error::COMMAND_FAILED); } switch (commandResult->commandId) { case LinkRawWireless::COMMAND_SIGNAL_LEVEL: { // SignalLevel (end) u32 levels = commandResult->dataSize > 0 ? commandResult->data[0] : 0; u32 players = 1; for (u32 i = 1; i < LINK_WIRELESS_MAX_PLAYERS; i++) { u32 level = (levels >> ((i - 1) * 8)) & 0xFF; sessionState.signalLevel.level[i] = level; if (level > 0) players++; } if (players > linkRawWireless.sessionState.playerCount) { LINK_BARRIER; linkRawWireless.sessionState.playerCount = Link::_min(players, config.maxPlayers); LINK_BARRIER; } break; } case LinkRawWireless::COMMAND_SEND_DATA: { // SendData (end) if (linkRawWireless.getState() == State::CONNECTED) sessionState.shouldWaitForServer = true; sessionState.sendReceiveLatch = !sessionState.sendReceiveLatch; break; } case LinkRawWireless::COMMAND_RECEIVE_DATA: { // ReceiveData (end) sessionState.sendReceiveLatch = sessionState.shouldWaitForServer || !sessionState.sendReceiveLatch; if (commandResult->dataSize == 0) break; sessionState.recvFlag = true; sessionState.recvTimeout = 0; sessionState.shouldWaitForServer = false; addIncomingMessagesFromData(commandResult); break; } default: { } } } LINK_WIRELESS_TIMER_ISR void checkConnectionsOrTransferData() { // (irq only) if (linkRawWireless.getState() == State::SERVING && !sessionState.signalLevelCalled) { // SignalLevel (start) if (sendCommandAsync(LinkRawWireless::COMMAND_SIGNAL_LEVEL)) sessionState.signalLevelCalled = true; } else if (linkRawWireless.getState() == State::CONNECTED || isConnected()) { bool shouldReceive = !sessionState.sendReceiveLatch || sessionState.shouldWaitForServer; if (shouldReceive) { // ReceiveData (start) sendCommandAsync(LinkRawWireless::COMMAND_RECEIVE_DATA); } else { // SendData (start) sendPendingData(); } } } LINK_WIRELESS_TIMER_ISR void sendPendingData() { // (irq only) copyOutgoingState(); setDataFromOutgoingMessages(); if (sendCommandAsync(LinkRawWireless::COMMAND_SEND_DATA, true)) clearInflightMessagesIfNeeded(); } LINK_WIRELESS_TIMER_ISR void setDataFromOutgoingMessages() { // (irq only) addAsyncData(0, true); // SendData header (filled later) addAsyncData(0); // Transfer header (filled later) bool isServer = linkRawWireless.getState() == State::SERVING; u32 maxPacketIds = isServer ? MAX_PACKET_IDS_SERVER : MAX_PACKET_IDS_CLIENT; u32 maxInflightPackets = isServer ? MAX_INFLIGHT_PACKETS_SERVER : MAX_INFLIGHT_PACKETS_CLIENT; u32 maxTransferLength = 1 + getDeviceTransferLength(); // (+1 for SendData header) u32 firstPacketId = NO_ID_ASSIGNED_YET; u32 firstMsg = 0; u32 msgCount = 0; bool highPart = false; u32 pendingForwardedCount = sessionState.forwardedCount; int currentPlayerBitMapIndex = -1; u32 playerBitMapCount = 0; sessionState.outgoingMessages.forEach( [this, isServer, maxPacketIds, maxInflightPackets, maxTransferLength, &firstPacketId, &firstMsg, &msgCount, &highPart, &pendingForwardedCount, ¤tPlayerBitMapIndex, &playerBitMapCount](Message* message) { // create packet ID if the packet can be sent if (message->packetId == NO_ID_ASSIGNED_YET) { if (sessionState.inflightCount < maxInflightPackets) { message->packetId = newPacketId(maxPacketIds); sessionState.inflightCount++; } else { return false; } } // get first added packet ID and add first msg if needed if (firstPacketId == NO_ID_ASSIGNED_YET) { firstPacketId = message->packetId; if (!isServer) { msgCount++; firstMsg = message->data; return true; } } // add a new PlayerBitMap if needed if (pendingForwardedCount > 0 && (currentPlayerBitMapIndex < 0 || playerBitMapCount == MAX_PLAYER_BITMAP_ENTRIES)) { if (playerBitMapCount == MAX_PLAYER_BITMAP_ENTRIES) addToAsyncDataShifted(currentPlayerBitMapIndex, 1, BIT_HAS_MORE); // `highPart` should always be false here! currentPlayerBitMapIndex = nextAsyncCommandDataSize; playerBitMapCount = 0; addAsyncData(0); highPart = true; } // add to the correct part of the u32 if (highPart) addToLastAsyncDataHalfword(message->data); else addAsyncData(message->data); highPart = !highPart; // update player bitmap if needed if (currentPlayerBitMapIndex >= 0) { addToAsyncDataShifted(currentPlayerBitMapIndex, message->playerId, PLAYER_ID_BITS * playerBitMapCount); playerBitMapCount++; if (message->playerId > 0) { pendingForwardedCount--; if (pendingForwardedCount == 0) currentPlayerBitMapIndex = -1; } } msgCount++; // only continue if we have available halfwords return nextAsyncCommandDataSize < maxTransferLength || highPart; }); // fill Transfer header nextAsyncCommandData[1] = buildTransferHeader(isServer, firstPacketId, firstMsg, msgCount, highPart); // fill SendData header u32 bytes = (nextAsyncCommandDataSize - 1) * 4; nextAsyncCommandData[0] = linkRawWireless.getSendDataHeaderFor(bytes); } u32 buildTransferHeader(bool isServer, u32 firstPacketId, u32 firstMsg, u32 msgCount, bool highPart) { // (irq only) TransferHeader transferHeader = {}; // player count / client heartbeat if (isServer) { transferHeader.playerCount = linkRawWireless.sessionState.playerCount - LINK_WIRELESS_MIN_PLAYERS; } else { sessionState.localHeartbeat = (sessionState.localHeartbeat + 1) % 4; transferHeader.playerCount = sessionState.localHeartbeat; } // first packet ID, or 0 if there are no messages if (msgCount > 0) transferHeader.firstPacketId = firstPacketId; // the last word has a message in its high part if there are messages and we // fully used all words transferHeader.hasLastMsg = msgCount > 0 && !highPart; // the transfer contains player bitmaps if there are forwarded messages transferHeader.hasPlayerBitMap = sessionState.forwardedCount > 0; // `ack1` is used by both server and clients transferHeader.ack1 = isServer ? sessionState.lastPacketIdFromClients[1] : sessionState.lastPacketIdFromServer; if (isServer) { // server use `ack2`, `ack3` and `ack4` for acknowledging P2, P3, P4 transferHeader.ack2 = sessionState.lastPacketIdFromClients[2]; transferHeader.ack3 = sessionState.lastPacketIdFromClients[3]; transferHeader.ack4 = sessionState.lastPacketIdFromClients[4]; } else if (msgCount > 0) { // but clients can use this area for storing the first message (*) transferHeader.firstPacketId |= HAS_FIRST_MSG_MASK; } // interpret the whole thing as u32 U32Packer packer = {}; packer.asStruct = transferHeader; if (!isServer) packer.asInt |= firstMsg; // (*) return packer.asInt; } void clearInflightMessagesIfNeeded() { // (irq only) if (config.retransmission) return; while (!sessionState.outgoingMessages.isEmpty()) { u32 packetId = sessionState.outgoingMessages.peek().packetId; if (packetId == NO_ID_ASSIGNED_YET) break; auto message = sessionState.outgoingMessages.pop(); if (linkRawWireless.getState() == State::SERVING && message.playerId > 0) sessionState.forwardedCount--; } sessionState.inflightCount = 0; } LINK_WIRELESS_SERIAL_ISR void addIncomingMessagesFromData( const CommandResult* result) { // (irq only) // parse ReceiveData header u32 sentBytes[LINK_WIRELESS_MAX_PLAYERS] = {0, 0, 0, 0, 0}; u32 receiveDataHeader = result->data[0]; sentBytes[0] = Link::_min(receiveDataHeader & 0b1111111, LinkRawWireless::MAX_TRANSFER_BYTES_SERVER); sentBytes[1] = Link::_min((receiveDataHeader >> 8) & 0b11111, LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT); sentBytes[2] = Link::_min((receiveDataHeader >> 13) & 0b11111, LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT); sentBytes[3] = Link::_min((receiveDataHeader >> 18) & 0b11111, LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT); sentBytes[4] = Link::_min((receiveDataHeader >> 23) & 0b11111, LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT); bool isServer = linkRawWireless.getState() == State::SERVING; u32 cursor = 1; u32 startPlayerId = isServer ? 1 : 0; u32 endPlayerId = isServer ? linkRawWireless.sessionState.playerCount : 1; // server reads from indexes 1~4, clients read from index 0 for (u32 i = startPlayerId; i < endPlayerId; i++) { if (sentBytes[i] % 4 != 0) return; // in our protocol, we always send whole words! u32 remainingWords = sentBytes[i] / 4; if (remainingWords == 0) continue; // parse TransferHeader U32Packer packer; packer.asInt = result->data[cursor++]; remainingWords--; TransferHeader header = packer.asStruct; // if retransmission is enabled, we update the confirmations based on the // ACKs found in the header if (config.retransmission) { if (isServer) { sessionState.lastAckFromClients[i] = header.ack1; } else { u32 currentPlayerId = linkRawWireless.sessionState.currentPlayerId; sessionState.lastAckFromServer = currentPlayerId == 1 ? header.ack1 : currentPlayerId == 2 ? header.ack2 : currentPlayerId == 3 ? header.ack3 : header.ack4; } } // clients update their player count based on the transfer header if (!isServer) { LINK_BARRIER; linkRawWireless.sessionState.playerCount = LINK_WIRELESS_MIN_PLAYERS + header.playerCount; LINK_BARRIER; } // clients can send their first message in the header itself u32 currentPacketId = header.firstPacketId; bool hasFirstMsg = isServer && (currentPacketId & HAS_FIRST_MSG_MASK) != 0; if (hasFirstMsg) { currentPacketId &= ~HAS_FIRST_MSG_MASK; u32 playerBitMap = 0; int playerBitMapCount = -1; processMessage(i, Link::lsB32(packer.asInt), currentPacketId, playerBitMap, playerBitMapCount); } // process the remaining words as message pairs u32 playerBitMap = 0; int playerBitMapCount = !isServer && header.hasPlayerBitMap ? MAX_PLAYER_BITMAP_ENTRIES : -1; while (remainingWords > 0) { bool hasHighPart = remainingWords > 1 || header.hasLastMsg; u32 word = result->data[cursor]; u32 lowPart = Link::lsB32(word); if (playerBitMapCount >= MAX_PLAYER_BITMAP_ENTRIES) { playerBitMap = lowPart; playerBitMapCount = 0; } else processMessage(i, lowPart, currentPacketId, playerBitMap, playerBitMapCount); if (hasHighPart) { u32 highPart = Link::msB32(word); processMessage(i, highPart, currentPacketId, playerBitMap, playerBitMapCount); } cursor++; remainingWords--; } bool shouldResetTimeouts = true; if (isServer) { // reset timeouts, only if the heartbeat from the clients changed (*) int heartbeat = header.playerCount; shouldResetTimeouts = heartbeat != sessionState.lastHeartbeatFromClients[i]; sessionState.lastHeartbeatFromClients[i] = heartbeat; // (*) sometimes, when a client is disconnected, the Wireless Adapter // keeps repeating old data in its slot! we use this heartbeat to verify // that the client is still generating packets actively! } if (shouldResetTimeouts) { sessionState.msgTimeouts[0] = 0; sessionState.msgTimeouts[i] = 0; sessionState.msgFlags[0] = true; sessionState.msgFlags[i] = true; } } // remove confirmed messages based on the updated ACKs if (config.retransmission) { if (isServer) removeConfirmedMessagesFromClients(); else removeConfirmedMessagesFromServer(); } // copy data from the interrupt world to the main world copyIncomingState(); } LINK_WIRELESS_ISR_FUNC( processMessage, (u32 playerId, u32 data, u32& currentPacketId, u32& playerBitMap, int& playerBitMapCount), (playerId, data, currentPacketId, playerBitMap, playerBitMapCount), { // (irq only) // store the packet ID and increment (msgs are consecutive inside // transfers) u32 packetId = currentPacketId; currentPacketId = (currentPacketId + 1) % (playerId == 0 ? MAX_PACKET_IDS_SERVER : MAX_PACKET_IDS_CLIENT); // get msg player ID based on player bitmap u32 msgPlayerId = playerId; if (playerBitMapCount >= 0) { msgPlayerId = (playerBitMap >> PLAYER_ID_BITS * playerBitMapCount) & PLAYER_ID_MASK; playerBitMapCount++; // (messages from remote player IDs 5, 6 and 7 could be received here, // but it's fine because `receive(...)` filters invalid entries) if (playerBitMapCount >= MAX_PLAYER_BITMAP_ENTRIES && !((playerBitMap >> BIT_HAS_MORE) & 1)) playerBitMapCount = -1; } if (playerId == 0 && !sessionState.didReceiveFirstPacketFromServer) { // the first time clients receive something from the server, // they shouldn't have any expectations (since they can join at any // time) sessionState.lastPacketIdFromServer = packetId; sessionState.didReceiveFirstPacketFromServer = true; } else { // if retransmission is enabled, the packet ID needs to be expected if (config.retransmission) { u32 expectedPacketId = playerId > 0 ? (sessionState.lastPacketIdFromClients[playerId] + 1) % MAX_PACKET_IDS_CLIENT : (sessionState.lastPacketIdFromServer + 1) % MAX_PACKET_IDS_SERVER; if (packetId != expectedPacketId) return; if (playerId > 0) sessionState.lastPacketIdFromClients[playerId] = expectedPacketId; else sessionState.lastPacketIdFromServer = expectedPacketId; } } // ignore messages from myself if (msgPlayerId == linkRawWireless.sessionState.currentPlayerId) return; // add new message Message message; message.playerId = msgPlayerId; message.data = data; message.packetId = packetId; sessionState.newIncomingMessages.push(message); // forward to other clients if needed if (playerId > 0 && config.forwarding && linkRawWireless.sessionState.playerCount > 2) forwardMessage(message); }) LINK_WIRELESS_SERIAL_ISR void forwardMessage( Message& message) { // (irq only) Message forwardedMessage; forwardedMessage.data = message.data; forwardedMessage.playerId = message.playerId; if (!sessionState.outgoingMessages.isFull()) { sessionState.outgoingMessages.push(forwardedMessage); sessionState.forwardedCount++; } else sessionState.outgoingMessages.overflow = true; } LINK_WIRELESS_SERIAL_ISR void removeConfirmedMessagesFromServer() { // (irq only) removeConfirmedMessages(sessionState.lastAckFromServer, MAX_PACKET_IDS_CLIENT, MAX_INFLIGHT_PACKETS_CLIENT); } LINK_WIRELESS_SERIAL_ISR void removeConfirmedMessagesFromClients() { // (irq only) u32 ringMinAck = 0xFFFFFFFF; for (u32 i = 1; i < linkRawWireless.sessionState.playerCount; i++) { u32 ack = sessionState.lastAckFromClients[i]; // ignore clients that didn't confirm anything yet if (ack == NO_ACK_RECEIVED_YET) continue; if (ringMinAck == 0xFFFFFFFF) { // on first time, we set `ringMinAck` ringMinAck = ack; } else { // we compare `ringMinAck` vs `ack` in circular space // (0..MAX_PACKET_IDS_CLIENT-1): // -> how many steps it is from `ringMinAck` down to `ack`? u32 dist = (ringMinAck - ack) & (MAX_PACKET_IDS_CLIENT - 1); // if dist >= MAX_INFLIGHT_PACKETS_CLIENT => `ack` is "behind" // `ringMinAck`, so we replace it! if (dist >= MAX_INFLIGHT_PACKETS_CLIENT) ringMinAck = ack; } } // if we found a valid minimum ack across all clients, we remove! if (ringMinAck != 0xFFFFFFFF) removeConfirmedMessages(ringMinAck, MAX_PACKET_IDS_SERVER, MAX_INFLIGHT_PACKETS_SERVER); } LINK_WIRELESS_SERIAL_ISR void removeConfirmedMessages( u32 ack, const u32 maxPacketIds, const u32 maxInflightPackets) { // (irq only) while (!sessionState.outgoingMessages.isEmpty()) { u32 packetId = sessionState.outgoingMessages.peek().packetId; // if the current message is not inflight, we've entered the section of // 'new' messages (with no ID assigned), so we quit! if (packetId == NO_ID_ASSIGNED_YET) break; // we release the packet if it was confirmed (aka inside the send window) // example with maxPacketIds=16, maxInflightPackets=7, ack=4: // => we would be releasing packets 4,3,2,1,15,14,13 if (((ack - packetId) & (maxPacketIds - 1)) <= maxInflightPackets) { auto message = sessionState.outgoingMessages.pop(); sessionState.inflightCount--; if (maxPacketIds == MAX_PACKET_IDS_SERVER && message.playerId > 0) sessionState.forwardedCount--; } else break; } } LINK_WIRELESS_TIMER_ISR u32 getDeviceTransferLength() { // (irq only) return linkRawWireless.getState() == State::SERVING ? LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH : LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH; } LINK_WIRELESS_TIMER_ISR void copyOutgoingState() { // (irq only) if (sessionState.newOutgoingMessages.isWriting()) return; while (!sessionState.newOutgoingMessages.isEmpty() && !sessionState.outgoingMessages.isFull()) { auto message = sessionState.newOutgoingMessages.pop(); sessionState.outgoingMessages.push(message); } } LINK_WIRELESS_SERIAL_ISR void copyIncomingState() { // (irq only) if (sessionState.incomingMessages.isReading()) return; while (!sessionState.newIncomingMessages.isEmpty() && !sessionState.incomingMessages.isFull()) { auto message = sessionState.newIncomingMessages.pop(); sessionState.incomingMessages.push(message); } } bool checkRemoteTimeouts() { // (irq only) bool isServer = linkRawWireless.getState() == State::SERVING; u32 startPlayerId = isServer ? 1 : 0; u32 endPlayerId = isServer ? linkRawWireless.sessionState.playerCount : 1; for (u32 i = startPlayerId; i < endPlayerId; i++) { if (!sessionState.msgFlags[i]) { sessionState.msgTimeouts[i]++; if (sessionState.msgTimeouts[i] > config.timeout) return false; } sessionState.msgFlags[i] = false; } return true; } LINK_WIRELESS_TIMER_ISR u32 newPacketId(u32 maxPacketIds) { // (irq only) return (sessionState.lastPacketId = (sessionState.lastPacketId + 1) % maxPacketIds); } LINK_WIRELESS_TIMER_ISR void addToLastAsyncDataHalfword( u16 value) { // (irq only) addToAsyncDataShifted(nextAsyncCommandDataSize - 1, value, 16); } LINK_WIRELESS_TIMER_ISR void addToAsyncDataShifted(u32 index, u16 value, u32 shift) { // (irq only) nextAsyncCommandData[index] |= value << shift; } LINK_WIRELESS_TIMER_ISR void addAsyncData(u32 value, bool start = false) { // (irq only) if (start) nextAsyncCommandDataSize = 0; nextAsyncCommandData[nextAsyncCommandDataSize] = value; nextAsyncCommandDataSize++; } bool sendCommandAsync(u8 type, bool withData = false) { // (irq only) if (isSendingSyncCommand) return false; u32 size = withData ? nextAsyncCommandDataSize : 0; return linkRawWireless.sendCommandAsync(type, nextAsyncCommandData, size, false, true); } bool isAsyncCommandActive() { return linkRawWireless.getAsyncState() == LinkRawWireless::AsyncState::WORKING; } bool badRequest(Error error) { isSendingSyncCommand = false; lastError = error; return false; } bool abort(Error error) { reset(); lastError = error; return false; } bool reset() { bool wasEnabled = isEnabled; LINK_BARRIER; isEnabled = false; LINK_BARRIER; resetState(); stop(); bool success = start(); LINK_BARRIER; isEnabled = wasEnabled; LINK_BARRIER; return success; } void resetState() { LINK_BARRIER; linkRawWireless._resetState(); sessionState.recvFlag = false; sessionState.recvTimeout = 0; sessionState.signalLevelCalled = false; sessionState.sendReceiveLatch = false; sessionState.shouldWaitForServer = false; sessionState.didReceiveFirstPacketFromServer = false; sessionState.inflightCount = 0; sessionState.forwardedCount = 0; sessionState.lastPacketId = 0; sessionState.lastPacketIdFromServer = 0; sessionState.lastAckFromServer = 0; sessionState.localHeartbeat = -1; sessionState.isResetTimeoutPending = false; for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++) { sessionState.msgTimeouts[i] = 0; sessionState.msgFlags[i] = false; sessionState.lastPacketIdFromClients[i] = 0; sessionState.lastAckFromClients[i] = NO_ACK_RECEIVED_YET; sessionState.lastHeartbeatFromClients[i] = -1; } nextAsyncCommandDataSize = 0; sessionState.incomingMessages.syncClear(); sessionState.outgoingMessages.clear(); sessionState.newIncomingMessages.clear(); sessionState.newOutgoingMessages.syncClear(); sessionState.newIncomingMessages.overflow = false; sessionState.signalLevel = SignalLevel{}; isSendingSyncCommand = false; LINK_BARRIER; } void stop() { stopTimer(); linkRawWireless.deactivate(); } bool start() { startTimer(); if (!linkRawWireless.activate(false)) return false; if (!setup()) return false; return true; } void stopTimer() { Link::_REG_TM[config.sendTimerId].cnt = Link::_REG_TM[config.sendTimerId].cnt & (~Link::_TM_ENABLE); } void startTimer() { Link::_REG_TM[config.sendTimerId].start = -config.interval; Link::_REG_TM[config.sendTimerId].cnt = Link::_TM_ENABLE | Link::_TM_IRQ | BASE_FREQUENCY; } bool setup(u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS) { return linkRawWireless.setup(maxPlayers); } template void waitVBlanks(u32 vBlanks, F onVBlank) { u32 count = 0; u32 vCount = Link::_REG_VCOUNT; while (count < vBlanks) { if (Link::_REG_VCOUNT != vCount) { vCount = Link::_REG_VCOUNT; if (vCount == 160) { onVBlank(); count++; } } }; } #ifdef LINK_WIRELESS_PROFILING_ENABLED void profileStart() { Link::_REG_TM1CNT_L = 0; Link::_REG_TM2CNT_L = 0; Link::_REG_TM1CNT_H = 0; Link::_REG_TM2CNT_H = 0; Link::_REG_TM2CNT_H = Link::_TM_ENABLE | Link::_TM_CASCADE; Link::_REG_TM1CNT_H = Link::_TM_ENABLE | Link::_TM_FREQ_1; } u32 profileStop() { Link::_REG_TM1CNT_H = 0; Link::_REG_TM2CNT_H = 0; return (Link::_REG_TM1CNT_L | (Link::_REG_TM2CNT_L << 16)); } #endif }; extern LinkWireless* linkWireless; /** * @brief VBLANK interrupt handler. */ inline void LINK_WIRELESS_ISR_VBLANK() { linkWireless->_onVBlank(); } /** * @brief SERIAL interrupt handler. */ inline void LINK_WIRELESS_ISR_SERIAL() { linkWireless->_onSerial(); } /** * @brief TIMER interrupt handler. */ inline void LINK_WIRELESS_ISR_TIMER() { linkWireless->_onTimer(); } /** * NOTES: * When using `LINK_WIRELESS_ENABLE_NESTED_IRQ`: * - Any user ISR can interrupt the library ISRs. * * The SERIAL ISR only enables nested interrupts after completing the * acknowledge with the Wireless Adapter. * - SERIAL ISR can interrupt TIMER ISR. * -> This doesn't cause data races since TIMER ISR only works when * there is no active async task. * -> When TIMER ISR starts an async task (`transferAsync(...)`), * nested interrupts are disabled (`REG_IME = 0`) and SERIAL cannot * interrupt anymore. * - TIMER interrupts are skipped if SERIAL ISR is running. * - VBLANK interrupts are postponed if SERIAL or TIMER ISRs are running. * - Nobody can interrupt VBLANK ISR. */ #endif // LINK_WIRELESS_H