mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-09-09 01:16:53 -05:00
Improving LinkCable's API to mimic LinkUniversal and better support mid-frame reads
This commit is contained in:
10
README.md
10
README.md
@@ -49,7 +49,7 @@ Name | Type | Default | Description
|
||||
`sendTimerId` | **u8** *(0~3)* | `3` | GBA Timer to use for sending.
|
||||
|
||||
You can also change these compile-time constants:
|
||||
- `LINK_CABLE_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outcoming messages the queues can store at max). The default value is `30`, which seems fine for most games.
|
||||
- `LINK_CABLE_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outgoing messages the queues can store at max **per player**). The default value is `15`, which seems fine for most games.
|
||||
|
||||
## Methods
|
||||
|
||||
@@ -61,9 +61,9 @@ Name | Return type | Description
|
||||
`isConnected()` | **bool** | Returns `true` if there are at least 2 connected players.
|
||||
`playerCount()` | **u8** *(0~4)* | Returns the number of connected players.
|
||||
`currentPlayerId()` | **u8** *(0~3)* | Returns the current player id.
|
||||
`sync()` | - | Call this method every time you need to fetch new data.
|
||||
`canRead(playerId)` | **bool** | Returns `true` if there are pending messages from player #`playerId`.
|
||||
`read(playerId)` | **u16** | Returns one message from player #`playerId`.
|
||||
`consume()` | - | Marks the current data as processed, enabling the library to fetch more.
|
||||
`send(data)` | - | Sends `data` to all connected players.
|
||||
|
||||
⚠️ `0xFFFF` and `0x0` are reserved values, so don't send them!
|
||||
@@ -165,7 +165,7 @@ Name | Type | Default | Description
|
||||
`asyncACKTimerId` | **s8** *(0~3 or -1)* | `-1` | GBA Timer to use for ACKs. If you have free timers, use one here to reduce CPU usage.
|
||||
|
||||
You can also change these compile-time constants:
|
||||
- `LINK_WIRELESS_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outcoming messages the queues can store at max). The default value is `30`, which seems fine for most games.
|
||||
- `LINK_WIRELESS_QUEUE_SIZE`: to set a custom buffer size (how many incoming and outgoing messages the queues can store at max). The default value is `30`, which seems fine for most games.
|
||||
- `LINK_WIRELESS_MAX_COMMAND_RESPONSE_LENGTH`: to set the biggest allowed response from the adapter. The default value is `50`, which allows reading all user messages (max receive length is `21`) and -in theory- up to `7` broadcasting servers *(7 values per broadcast * 7 = 49 responses)*. This library was only tested with `4` adapters, so the real maximum is unknown.
|
||||
- `LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH` and `LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH`: to set the biggest allowed transfer per timer tick. Transfers contain retransmission headers and multiple user messages. These values must be in the range `[6;20]` for servers and `[2;4]` for clients. The default values are `20` and `4`, but you might want to set them a bit lower to reduce CPU usage.
|
||||
|
||||
@@ -217,9 +217,7 @@ Name | Type | Default | Description
|
||||
|
||||
## Methods
|
||||
|
||||
The interface is the same as [👾 LinkCable](#methods), with one exception: instead of calling `consume()` at the end of your game loop, you call `sync()` at the start.
|
||||
|
||||
Aditionally, it supports these methods:
|
||||
The interface is the same as [👾 LinkCable](#methods). Aditionally, it supports these methods:
|
||||
|
||||
Name | Return type | Description
|
||||
--- | --- | ---
|
||||
|
||||
@@ -96,6 +96,7 @@ int main() {
|
||||
// Client mode
|
||||
// ---
|
||||
|
||||
linkCable->sync();
|
||||
linkCable->send(keys + 1);
|
||||
|
||||
std::string output = "";
|
||||
@@ -119,8 +120,6 @@ int main() {
|
||||
output += std::string("Waiting... ");
|
||||
}
|
||||
|
||||
linkCable->consume();
|
||||
|
||||
VBlankIntrWait();
|
||||
log(output);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ int main() {
|
||||
data[i] = 0;
|
||||
|
||||
while (true) {
|
||||
// (4) Send/read messages
|
||||
// (4) Sync
|
||||
linkCable->sync();
|
||||
|
||||
// (5) Send/read messages
|
||||
u16 keys = ~REG_KEYS & KEY_ANY;
|
||||
linkCable->send(keys + 1); // (avoid using 0)
|
||||
|
||||
@@ -63,9 +66,6 @@ int main() {
|
||||
output += std::string("Waiting...");
|
||||
}
|
||||
|
||||
// (5) Mark the current state copy (front buffer) as consumed
|
||||
linkCable->consume();
|
||||
|
||||
VBlankIntrWait();
|
||||
log(output);
|
||||
}
|
||||
|
||||
@@ -42,10 +42,8 @@ void TestScene::tick(u16 keys) {
|
||||
if (engine->isTransitioning())
|
||||
return;
|
||||
|
||||
#ifdef USE_LINK_UNIVERSAL
|
||||
// sync
|
||||
link->sync();
|
||||
#endif
|
||||
|
||||
frameCounter++;
|
||||
|
||||
@@ -103,9 +101,4 @@ void TestScene::tick(u16 keys) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef USE_LINK_UNIVERSAL
|
||||
// mark link buffer as consumed
|
||||
link->consume();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ int main() {
|
||||
bool error = false;
|
||||
|
||||
while (true) {
|
||||
linkCable->sync();
|
||||
|
||||
std::string output = "";
|
||||
if (linkCable->isConnected()) {
|
||||
auto playerCount = linkCable->playerCount();
|
||||
@@ -68,8 +70,6 @@ int main() {
|
||||
error = false;
|
||||
}
|
||||
|
||||
linkCable->consume();
|
||||
|
||||
VBlankIntrWait();
|
||||
log(output);
|
||||
|
||||
|
||||
155
lib/LinkCable.h
155
lib/LinkCable.h
@@ -14,6 +14,9 @@
|
||||
// irq_add(II_TIMER3, LINK_CABLE_ISR_TIMER);
|
||||
// - 3) Initialize the library with:
|
||||
// linkCable->activate();
|
||||
// - 4) Sync:
|
||||
// linkUniversal->sync();
|
||||
// // (put this line at the start of your game loop)
|
||||
// - 4) Send/read messages by using:
|
||||
// bool isConnected = linkCable->isConnected();
|
||||
// u8 playerCount = linkCable->playerCount();
|
||||
@@ -23,9 +26,6 @@
|
||||
// u16 message = linkCable->read(!currentPlayerId);
|
||||
// // ...
|
||||
// }
|
||||
// - 5) Mark the current state copy (front buffer) as consumed:
|
||||
// linkCable->consume();
|
||||
// // (put this line at the end of your game loop)
|
||||
// --------------------------------------------------------------------------
|
||||
// (*) libtonc's interrupt handler sometimes ignores interrupts due to a bug.
|
||||
// That can cause packet loss. You might want to use libugba's instead.
|
||||
@@ -39,7 +39,7 @@
|
||||
#include <tonc_core.h>
|
||||
|
||||
// Buffer size
|
||||
#define LINK_CABLE_QUEUE_SIZE 30
|
||||
#define LINK_CABLE_QUEUE_SIZE 15
|
||||
|
||||
#define LINK_CABLE_MAX_PLAYERS 4
|
||||
#define LINK_CABLE_DISCONNECTED 0xFFFF
|
||||
@@ -63,7 +63,7 @@
|
||||
#define LINK_CABLE_SET_LOW(REG, BIT) REG &= ~(1 << BIT)
|
||||
#define LINK_CABLE_BARRIER asm volatile("" ::: "memory")
|
||||
|
||||
static volatile char LINK_CABLE_VERSION[] = "LinkCable/v5.1.1";
|
||||
static volatile char LINK_CABLE_VERSION[] = "LinkCable/v6.0.0";
|
||||
|
||||
void LINK_CABLE_ISR_VBLANK();
|
||||
void LINK_CABLE_ISR_SERIAL();
|
||||
@@ -103,8 +103,8 @@ class LinkCable {
|
||||
}
|
||||
|
||||
void clear() {
|
||||
while (!isEmpty())
|
||||
pop();
|
||||
front = count = 0;
|
||||
rear = -1;
|
||||
}
|
||||
|
||||
int size() { return count; }
|
||||
@@ -133,46 +133,49 @@ class LinkCable {
|
||||
bool isActive() { return isEnabled; }
|
||||
|
||||
void activate() {
|
||||
isEnabled = false;
|
||||
reset();
|
||||
isEnabled = true;
|
||||
}
|
||||
|
||||
void deactivate() {
|
||||
isEnabled = false;
|
||||
isStateReady = false;
|
||||
isStateConsumed = false;
|
||||
isResetting = false;
|
||||
resetState();
|
||||
stop();
|
||||
}
|
||||
|
||||
bool isConnected() {
|
||||
return $state.playerCount > 1 &&
|
||||
$state.currentPlayerId < $state.playerCount;
|
||||
return state.playerCount > 1 && state.currentPlayerId < state.playerCount;
|
||||
}
|
||||
|
||||
u8 playerCount() { return $state.playerCount; }
|
||||
u8 currentPlayerId() { return $state.currentPlayerId; }
|
||||
u8 playerCount() { return state.playerCount; }
|
||||
u8 currentPlayerId() { return state.currentPlayerId; }
|
||||
|
||||
void sync() {
|
||||
if (!isEnabled)
|
||||
return;
|
||||
|
||||
LINK_CABLE_BARRIER;
|
||||
isReadingMessages = true;
|
||||
LINK_CABLE_BARRIER;
|
||||
|
||||
for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++)
|
||||
move(_state.pendingMessages[i], state.incomingMessages[i]);
|
||||
|
||||
LINK_CABLE_BARRIER;
|
||||
isReadingMessages = false;
|
||||
LINK_CABLE_BARRIER;
|
||||
|
||||
if (!isConnected())
|
||||
for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++)
|
||||
state.incomingMessages[i].clear();
|
||||
}
|
||||
|
||||
bool canRead(u8 playerId) {
|
||||
if (!isStateReady || isStateConsumed)
|
||||
return false;
|
||||
|
||||
LINK_CABLE_BARRIER;
|
||||
|
||||
return !$state.incomingMessages[playerId].isEmpty();
|
||||
return !state.incomingMessages[playerId].isEmpty();
|
||||
}
|
||||
|
||||
u16 read(u8 playerId) {
|
||||
if (!isStateReady || isStateConsumed)
|
||||
return LINK_CABLE_NO_DATA;
|
||||
|
||||
LINK_CABLE_BARRIER;
|
||||
|
||||
return $state.incomingMessages[playerId].pop();
|
||||
}
|
||||
|
||||
void consume() { isStateConsumed = true; }
|
||||
u16 read(u8 playerId) { return state.incomingMessages[playerId].pop(); }
|
||||
|
||||
void send(u16 data) {
|
||||
if (data == LINK_CABLE_DISCONNECTED || data == LINK_CABLE_NO_DATA)
|
||||
@@ -188,9 +191,9 @@ class LinkCable {
|
||||
isAddingMessage = false;
|
||||
LINK_CABLE_BARRIER;
|
||||
|
||||
if (isResetting) {
|
||||
if (isAddingWhileResetting) {
|
||||
_state.outgoingMessages.clear();
|
||||
isResetting = false;
|
||||
isAddingWhileResetting = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,8 +213,8 @@ class LinkCable {
|
||||
if (!isEnabled)
|
||||
return;
|
||||
|
||||
if (resetIfNeeded()) {
|
||||
copyState();
|
||||
if (!isReady() || hasError()) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,17 +227,18 @@ class LinkCable {
|
||||
|
||||
if (data != LINK_CABLE_DISCONNECTED) {
|
||||
if (data != LINK_CABLE_NO_DATA && i != state.currentPlayerId)
|
||||
state.incomingMessages[i].push(data);
|
||||
_state.tmpMessagesToReceive[i].push(data);
|
||||
newPlayerCount++;
|
||||
_state.timeouts[i] = 0;
|
||||
} else if (_state.timeouts[i] > LINK_CABLE_REMOTE_TIMEOUT_OFFLINE) {
|
||||
setOnline(i);
|
||||
} else if (isOnline(i)) {
|
||||
_state.timeouts[i]++;
|
||||
|
||||
if (_state.timeouts[i] >= (int)config.remoteTimeout) {
|
||||
state.incomingMessages[i].clear();
|
||||
_state.timeouts[i] = LINK_CABLE_REMOTE_TIMEOUT_OFFLINE;
|
||||
} else
|
||||
_state.tmpMessagesToReceive[i].clear();
|
||||
setOffline(i);
|
||||
} else {
|
||||
newPlayerCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +259,6 @@ class LinkCable {
|
||||
|
||||
if (didTimeout()) {
|
||||
reset();
|
||||
copyState();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -282,20 +285,20 @@ class LinkCable {
|
||||
|
||||
struct InternalState {
|
||||
U16Queue outgoingMessages;
|
||||
U16Queue pendingMessages[LINK_CABLE_MAX_PLAYERS];
|
||||
U16Queue tmpMessagesToReceive[LINK_CABLE_MAX_PLAYERS];
|
||||
int timeouts[LINK_CABLE_MAX_PLAYERS];
|
||||
bool IRQFlag;
|
||||
u32 IRQTimeout;
|
||||
};
|
||||
|
||||
ExternalState state; // (updated state / back buffer)
|
||||
ExternalState $state; // (visible state / front buffer)
|
||||
InternalState _state; // (internal state)
|
||||
ExternalState state;
|
||||
InternalState _state;
|
||||
Config config;
|
||||
bool isEnabled = false;
|
||||
volatile bool isStateReady = false;
|
||||
volatile bool isStateConsumed = false;
|
||||
volatile bool isEnabled = false;
|
||||
volatile bool isReadingMessages = false;
|
||||
volatile bool isAddingMessage = false;
|
||||
volatile bool isResetting = false;
|
||||
volatile bool isAddingWhileResetting = false;
|
||||
|
||||
bool isReady() { return isBitHigh(LINK_CABLE_BIT_READY); }
|
||||
bool hasError() { return isBitHigh(LINK_CABLE_BIT_ERROR); }
|
||||
@@ -319,15 +322,6 @@ class LinkCable {
|
||||
setBitHigh(LINK_CABLE_BIT_START);
|
||||
}
|
||||
|
||||
bool resetIfNeeded() {
|
||||
if (!isReady() || hasError()) {
|
||||
reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
resetState();
|
||||
stop();
|
||||
@@ -337,17 +331,21 @@ class LinkCable {
|
||||
void resetState() {
|
||||
state.playerCount = 0;
|
||||
state.currentPlayerId = 0;
|
||||
|
||||
if (isAddingMessage)
|
||||
isAddingWhileResetting = true;
|
||||
else
|
||||
_state.outgoingMessages.clear();
|
||||
|
||||
for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) {
|
||||
state.incomingMessages[i].clear();
|
||||
_state.timeouts[i] = LINK_CABLE_REMOTE_TIMEOUT_OFFLINE;
|
||||
if (!isReadingMessages)
|
||||
_state.pendingMessages[i].clear();
|
||||
|
||||
_state.tmpMessagesToReceive[i].clear();
|
||||
setOffline(i);
|
||||
}
|
||||
_state.IRQFlag = false;
|
||||
_state.IRQTimeout = 0;
|
||||
|
||||
if (isAddingMessage || isResetting)
|
||||
isResetting = true;
|
||||
else
|
||||
_state.outgoingMessages.clear();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
@@ -379,21 +377,28 @@ class LinkCable {
|
||||
}
|
||||
|
||||
void copyState() {
|
||||
if (isStateReady && !isStateConsumed)
|
||||
if (isReadingMessages)
|
||||
return;
|
||||
|
||||
LINK_CABLE_BARRIER;
|
||||
$state.playerCount = state.playerCount;
|
||||
$state.currentPlayerId = state.currentPlayerId;
|
||||
for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) {
|
||||
$state.incomingMessages[i].clear();
|
||||
while (!state.incomingMessages[i].isEmpty())
|
||||
$state.incomingMessages[i].push(state.incomingMessages[i].pop());
|
||||
if (isOnline(i))
|
||||
move(_state.tmpMessagesToReceive[i], _state.pendingMessages[i]);
|
||||
else
|
||||
_state.pendingMessages[i].clear();
|
||||
}
|
||||
LINK_CABLE_BARRIER;
|
||||
isStateReady = true;
|
||||
isStateConsumed = false;
|
||||
LINK_CABLE_BARRIER;
|
||||
}
|
||||
|
||||
void move(U16Queue& src, U16Queue& dst) {
|
||||
while (!src.isEmpty())
|
||||
dst.push(src.pop());
|
||||
}
|
||||
|
||||
bool isOnline(u8 playerId) {
|
||||
return _state.timeouts[playerId] != LINK_CABLE_REMOTE_TIMEOUT_OFFLINE;
|
||||
}
|
||||
void setOnline(u8 playerId) { _state.timeouts[playerId] = 0; }
|
||||
void setOffline(u8 playerId) {
|
||||
_state.timeouts[playerId] = LINK_CABLE_REMOTE_TIMEOUT_OFFLINE;
|
||||
}
|
||||
|
||||
bool isBitHigh(u8 bit) { return (REG_SIOCNT >> bit) & 1; }
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
return error(FAILURE_DURING_HANDSHAKE);
|
||||
|
||||
static volatile char LINK_CABLE_MULTIBOOT_VERSION[] =
|
||||
"LinkCableMultiboot/v5.1.1";
|
||||
"LinkCableMultiboot/v6.0.0";
|
||||
|
||||
const u8 LINK_CABLE_MULTIBOOT_CLIENT_IDS[] = {0b0010, 0b0100, 0b1000};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
#define LINK_GPIO_SET_HIGH(REG, BIT) REG |= 1 << BIT
|
||||
#define LINK_GPIO_SET_LOW(REG, BIT) REG &= ~(1 << BIT)
|
||||
|
||||
static volatile char LINK_GPIO_VERSION[] = "LinkGPIO/v5.1.1";
|
||||
static volatile char LINK_GPIO_VERSION[] = "LinkGPIO/v6.0.0";
|
||||
|
||||
const u8 LINK_GPIO_DATA_BITS[] = {2, 3, 1, 0};
|
||||
const u8 LINK_GPIO_DIRECTION_BITS[] = {6, 7, 5, 4};
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
#define LINK_SPI_SET_HIGH(REG, BIT) REG |= 1 << BIT
|
||||
#define LINK_SPI_SET_LOW(REG, BIT) REG &= ~(1 << BIT)
|
||||
|
||||
static volatile char LINK_SPI_VERSION[] = "LinkSPI/v5.1.1";
|
||||
static volatile char LINK_SPI_VERSION[] = "LinkSPI/v6.0.0";
|
||||
|
||||
class LinkSPI {
|
||||
public:
|
||||
@@ -192,7 +192,7 @@ class LinkSPI {
|
||||
bool waitMode = false;
|
||||
AsyncState asyncState = IDLE;
|
||||
u32 asyncData = 0;
|
||||
bool isEnabled = false;
|
||||
volatile bool isEnabled = false;
|
||||
|
||||
void setNormalMode() {
|
||||
LINK_SPI_SET_LOW(REG_RCNT, LINK_SPI_BIT_GENERAL_PURPOSE_HIGH);
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
#define LINK_UNIVERSAL_SERVE_WAIT_FRAMES 60
|
||||
#define LINK_UNIVERSAL_SERVE_WAIT_FRAMES_RANDOM 30
|
||||
|
||||
static volatile char LINK_UNIVERSAL_VERSION[] = "LinkUniversal/v5.1.1";
|
||||
static volatile char LINK_UNIVERSAL_VERSION[] = "LinkUniversal/v6.0.0";
|
||||
|
||||
void LINK_UNIVERSAL_ISR_VBLANK();
|
||||
void LINK_UNIVERSAL_ISR_SERIAL();
|
||||
@@ -153,6 +153,9 @@ class LinkUniversal {
|
||||
__qran_seed += REG_RCNT;
|
||||
__qran_seed += REG_SIOCNT;
|
||||
|
||||
if (mode == LINK_CABLE)
|
||||
linkCable->sync();
|
||||
|
||||
switch (state) {
|
||||
case INITIALIZING: {
|
||||
waitCount++;
|
||||
@@ -209,9 +212,6 @@ class LinkUniversal {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == LINK_CABLE)
|
||||
linkCable->consume();
|
||||
}
|
||||
|
||||
bool canRead(u8 playerId) { return !incomingMessages[playerId].isEmpty(); }
|
||||
@@ -282,7 +282,7 @@ class LinkUniversal {
|
||||
u32 switchWait = 0;
|
||||
u32 subWaitCount = 0;
|
||||
u32 serveWait = 0;
|
||||
bool isEnabled = false;
|
||||
volatile bool isEnabled = false;
|
||||
|
||||
void receiveCableMessages() {
|
||||
for (u32 i = 0; i < LINK_UNIVERSAL_MAX_PLAYERS; i++) {
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
if (!reset()) \
|
||||
return false;
|
||||
|
||||
static volatile char LINK_WIRELESS_VERSION[] = "LinkWireless/v5.1.1";
|
||||
static volatile char LINK_WIRELESS_VERSION[] = "LinkWireless/v6.0.0";
|
||||
|
||||
void LINK_WIRELESS_ISR_VBLANK();
|
||||
void LINK_WIRELESS_ISR_SERIAL();
|
||||
@@ -661,8 +661,8 @@ class LinkWireless {
|
||||
}
|
||||
|
||||
void clear() {
|
||||
while (!isEmpty())
|
||||
pop();
|
||||
front = count = 0;
|
||||
rear = -1;
|
||||
}
|
||||
|
||||
int size() { return count; }
|
||||
@@ -761,7 +761,7 @@ class LinkWireless {
|
||||
volatile bool isAddingMessage = false;
|
||||
volatile bool isPendingClearActive = false;
|
||||
Error lastError = NONE;
|
||||
bool isEnabled = false;
|
||||
volatile bool isEnabled = false;
|
||||
|
||||
void forwardMessageIfNeeded(Message& message) {
|
||||
if (state == SERVING && config.forwarding && sessionState.playerCount > 2)
|
||||
|
||||
Reference in New Issue
Block a user