Improving LinkCable's API to mimic LinkUniversal and better support mid-frame reads

This commit is contained in:
Rodrigo Alfonso
2023-11-15 01:20:18 -03:00
parent fe572b0f74
commit e12ffdd5fa
11 changed files with 104 additions and 109 deletions

View File

@@ -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
--- | --- | ---

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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; }

View File

@@ -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};

View File

@@ -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};

View File

@@ -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);

View File

@@ -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++) {

View File

@@ -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)