Poke_Transporter_GB/source/link_handler.cpp
Philippe Symons 0177374f00 Fix broken Write Cbl Data - CART bug.
For some reason, the merge broke the Write Cbl Data - CART functionality in the dbg menu.

The main problem is that writing a whole sector of data and erasing a sector is too slow to do in an IRQ handler.

Fix: Move the actual flash writing process to the main loop by creating a function called handleCartIO().
The IRQ handler writes to global_memory_buffer, and the handleCartIO() copies that
data to a local write buffer while the IRQ may still add more to global_memory_buffer.

Then it modifies the IRQ handlers' offset and moves the data that was appended in between to the start of the buffer all while
the IRQ handler is still appending new bytes.

If we end up at a sector boundary, we also do the erase_sector() call.

This was tested with and without the print link and print packets debug options.
Without the print options enabled, we end up occupying about 20KB on the cartridge save. (for 1st stage only!)

Man, this bug sucked. I rate it 1 star!
2026-06-08 23:01:31 +02:00

725 lines
19 KiB
C++

#include <tonc.h>
#include <stdarg.h>
#include <inttypes.h>
#include "libraries/nanoprintf/nanoprintf.h"
#include "libstd_replacements.h"
#include "link_handler.h"
#include "script_array.h"
#include "dbg/debug_mode.h"
#include "interrupt.h"
#include "text_engine.h"
#include "global_frame_controller.h"
#include "background_engine.h"
#include "sprite_data.h"
#include "libraries/Pokemon-Gen3-to-Gen-X/include/save.h"
#include "flash_mem.h"
#include "FileContainerReader.h"
#include "text_tables.h"
#include "translated_text.h"
#include "GB_Payloads_chunk0_lz10_bin.h"
#include "GB_Payloads.h"
LinkSPI linkSPIInstance;
LinkSPI *linkSPI = &linkSPIInstance;
// Here's a compilation check to ensure that the size of these structs match our expectations.
// Just update it if you changed the struct members. The data-generator process prints their actual sizes.
static_assert(sizeof(struct GB_ROM) == 136);
static_assert(sizeof(struct ROM_DATA) == 160);
LinkConnection globalLinkCable;
void linkCableIRQ()
{
if (!globalLinkCable.earlyExit())
{
globalLinkCable.exchangeBytes();
if (g_debug_options.print_link_data || g_debug_options.print_link_packets)
{
globalLinkCable.printData();
}
if (g_debug_options.write_cable_data_to_save)
{
globalLinkCable.writeData();
}
globalLinkCable.handleStateLogic();
}
}
void LinkConnection::setup(const u16 *debug_charset)
{
link_cable_memory_section_index = 0;
link_cable_array_index = 0;
writeBufferOffset = 0;
linkSPI->activate(LinkSPI::Mode::MASTER_256KBPS);
linkSPI->setWaitModeActive(false);
this->debug_charset = debug_charset;
if (g_debug_options.print_link_data == true)
{
create_textbox(0, 0, 138, 128, false);
}
if(g_debug_options.write_cable_data_to_save == WRITE_CABLE_DATA_MODE_SRAM)
{
// if we're writing the cable data to SRAM, we should clear the SRAM first to make sure there's no leftover data from previous transfers
volatile u8 *cur = SRAM_PTR;
volatile u8 *end = SRAM_PTR + 0x10000;
while(cur < end)
{
(*cur) = 0;
++cur;
}
}
else if(g_debug_options.write_cable_data_to_save == WRITE_CABLE_DATA_MODE_CART)
{
// before each write, we need to erase the sector.
// so, let's do that for the first one before we start writing anything.
erase_sector(0);
}
}
void LinkConnection::startConnection(CompositeState startState)
{
switch (startState)
{
case INITIAL_CONNECTION:
subState = CLOCK;
REG_TM3D = -0x4000 / 60;
REG_TM3CNT = TM_FREQ_1024 | TM_ENABLE;
break;
case PACKET_EXCHANGE:
subState = BYTE_EXCHANGE;
REG_TM3D = -0x0040;
// REG_TM3D = -0x4000 / 2;
REG_TM3CNT = TM_FREQ_1024 | TM_ENABLE;
break;
default:
break;
}
nextSubState = subState;
nextCompState = startState;
lastError = NO_ERROR;
irq_enable(II_TIMER3);
}
void LinkConnection::exchangeBytes()
{
/*
int timeout_frames = 10;
inData = linkSPI->transfer(outData, [&timeout_frames]()
{
// In the mGBA Lua bridge, replies arrive via emulator callbacks between frames.
// Waiting here prevents valid bytes from being reported as timeouts.
global_next_frame();
return --timeout_frames <= 0; });
*/
switch(g_debug_options.load_cable_data_from_save)
{
case WRITE_CABLE_DATA_MODE_OFF:
// Normal transfer :-)
inData = linkSPI->transfer(outData);
break;
case WRITE_CABLE_DATA_MODE_SRAM:
// Pretend transfer, by loading the bytes from SRAM (where we stored them with writeData() in a previous transfer)
inData = (*(SRAM_PTR + link_cable_array_index));
++link_cable_array_index;
outData = (*(SRAM_PTR + link_cable_array_index));
++link_cable_array_index;
break;
case WRITE_CABLE_DATA_MODE_CART:
{
// Pretend transfer, by loading the bytes from the cartridge save. (where we stored them with writeData() in a previous transfer)
// skip the first 6 bytes, which are for human consumption
link_cable_array_index += 6;
inData = read_byte_save((0x1000 * link_cable_memory_section_index) + link_cable_array_index);
++link_cable_array_index;
outData = read_byte_save((0x1000 * link_cable_memory_section_index) + link_cable_array_index);
++link_cable_array_index;
if(link_cable_array_index >= 0x1000)
{
// if we reached the end of the section, we need to load the next section (if there is one)
++link_cable_memory_section_index;
link_cable_array_index = 0;
}
break;
}
}
}
void LinkConnection::printData()
{
if (globalLinkCable.skipPrint)
{
tte_erase_rect(0, 0, H_MAX, V_MAX);
}
else
{
if (g_debug_options.print_link_data)
{
n2hexstr(&line[0], compState & 0xFF, 2);
line[2] = ':';
n2hexstr(&line[3], compStateCounter & 0xFFFF, 4);
line[7] = '|';
n2hexstr(&line[8], subState & 0xFF, 2);
line[10] = ':';
n2hexstr(&line[11], subStateCounter & 0xFFFF, 4);
line[15] = '|';
line[16] = 'i';
n2hexstr(&line[17], inData & 0xFF, 2);
line[19] = '|';
line[20] = 'o';
n2hexstr(&line[21], outData & 0xFF, 2);
line[23] = '\0';
scroll_text(true, tte_get_context(), false, 8, 8, 138, 135);
ptgb_write_debug(this->debug_charset, line, true);
}
// TODO: This is pretty rough, but it's the best we can do until the text engine rewrite.
if (g_debug_options.print_link_packets)
{
tte_erase_rect(160, 16, H_MAX, V_MAX);
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
n2hexstr(&line[3 * j], dataOutBuffer[(4 * i) + j], 2);
line[(3 * j) + 2] = ' ';
}
line[12] = '\0';
tte_set_pos(160, 16 * i);
ptgb_write_debug(this->debug_charset, line, true);
}
for (int i = 0; i < 4; i++)
{
byte tempBuffer[16];
int packetIndex = dataOutBuffer[INP_COUNTER_INDEX];
LinkPacket &currLinkPacket = currLinkPacketArr[packetIndex];
tempBuffer[0] = packetIndex;
tempBuffer[1] = currLinkPacket.command;
tempBuffer[2] = currLinkPacket.pointer >> 0;
tempBuffer[3] = currLinkPacket.pointer >> 8;
memcpy(&tempBuffer[4], currLinkPacket.argument, 2);
tempBuffer[6] = currLinkPacket.latestError;
tempBuffer[7] = 0x00;
memcpy(&tempBuffer[8], currLinkPacket.recievedData, 8);
for (int j = 0; j < 4; j++)
{
n2hexstr(&line[3 * j], tempBuffer[(4 * i) + j], 2);
line[(3 * j) + 2] = ' ';
}
line[12] = '\0';
tte_set_pos(160, (16 * 5) + (16 * i));
ptgb_write_debug(this->debug_charset, line, true);
}
}
}
}
void LinkConnection::writeData()
{
switch(g_debug_options.write_cable_data_to_save)
{
case WRITE_CABLE_DATA_MODE_OFF:
break;
case WRITE_CABLE_DATA_MODE_SRAM:
{
(*(SRAM_PTR + link_cable_array_index)) = inData;
++link_cable_array_index;
(*(SRAM_PTR + link_cable_array_index)) = outData;
++link_cable_array_index;
break;
}
case WRITE_CABLE_DATA_MODE_CART:
{
// save the data to the cartridge in chunks of 4 KB
// WARNING: If you want to add or remove fields here,
// make sure to keep the number of bytes a clean divider of 4096 (global_memory_buffer_size)
// the next 6 bytes are for human consumption when viewed in a hex editor.
// they can be useful to correlate the current LinkConnection state with the data that was sent over the cable.
// but they're not needed for reconstructing the conversation with load_cable_data_from_save
global_memory_buffer[writeBufferOffset + 0] = (u8)compState;
global_memory_buffer[writeBufferOffset + 1] = (u8)((compStateCounter >> 8) & 0xFF);
global_memory_buffer[writeBufferOffset + 2] = (u8)((compStateCounter >> 0) & 0xFF);
global_memory_buffer[writeBufferOffset + 3] = (u8)subState;
global_memory_buffer[writeBufferOffset + 4] = (u8)((subStateCounter >> 8) & 0xFF);
global_memory_buffer[writeBufferOffset + 5] = (u8)((subStateCounter >> 0) & 0xFF);
// actual data bytes start here.
global_memory_buffer[writeBufferOffset + 6] = inData;
global_memory_buffer[writeBufferOffset + 7] = outData;
writeBufferOffset += 8;
break;
}
}
}
void LinkConnection::handleStateLogic()
{
subState = nextSubState;
compState = nextCompState;
switch (compState)
{
case INITIAL_CONNECTION:
logicState_initConnection();
break;
case PACKET_EXCHANGE:
logicState_packetExchange();
break;
default:
break;
}
if (nextSubState != subState)
{
subStateCounter = 0;
subStateChanged = true;
}
else
{
subStateCounter++;
subStateChanged = false;
}
if (nextCompState != compState)
{
compStateCounter = 0;
compStateChanged = true;
}
else
{
compStateCounter++;
compStateChanged = false;
}
}
/*
* So, dealing with writing to the cartridge in the IRQ handler was a no-go.
* It just caused too many issues with data corruption, probably because the write and erase_sector
* operation was taking too long.
*
* So, I moved it to the main loop through this function.
* However, we need to be aware that this function can get interrupted by the IRQ handler at any time.
*
* We also need to take care to not have global_memory_buffer overflow as the IRQ handler just keeps adding to it.
*/
void LinkConnection::handleCartIO()
{
u8 writeBuffer[0x1000];
unsigned curBufDepth = writeBufferOffset;
// first copy the current data to a local buffer. Note: the IRQ handler could append new data to global_memory_buffer during this call!
memcpy(writeBuffer, global_memory_buffer, curBufDepth);
// by updating the writeBufferOffset already, we allow the IRQ handler to start writing at the new right position
// immediately.
writeBufferOffset -= curBufDepth;
// now move any data received during the memcpy to before writeBufferOffset, so it will be included in the next batch.
// Note: keep in mind, here too the IRQ handler may be appending new data to global_memory_buffer and increase writeBufferOffset.
// but it's harmless.
memmove(global_memory_buffer, global_memory_buffer + curBufDepth, writeBufferOffset);
u8 *curWriteBuf = writeBuffer;
const u8 * const endWriteBuf = writeBuffer + curBufDepth;
while(curWriteBuf < endWriteBuf)
{
// make sure not to write beyond the current flash sector's boundaries. We'll need an erase_sector() call before
// we write to the next sector.
const unsigned bytesRemainingInSector = 0x1000 - link_cable_array_index;
const unsigned bytesToWrite = (curBufDepth < bytesRemainingInSector) ? curBufDepth : bytesRemainingInSector;
copy_ram_to_save(curWriteBuf, (0x1000 * link_cable_memory_section_index) + link_cable_array_index, bytesToWrite);
curWriteBuf += bytesToWrite;
curBufDepth -= bytesToWrite;
link_cable_array_index += bytesToWrite;
if(link_cable_array_index >= 0x1000)
{
// we have reached the end of our current sector. Let's erase the next one.
link_cable_array_index = 0;
++link_cable_memory_section_index;
erase_sector(0x1000 * link_cable_memory_section_index);
}
}
}
bool LinkConnection::earlyExit()
{
if (g_debug_options.print_link_data && !skipPrint && key_held(KEY_LEFT))
{
pauseOnByte = true;
pauseOnPacket = false;
}
else if (g_debug_options.print_link_packets && !skipPrint && key_held(KEY_RIGHT))
{
pauseOnPacket = true;
pauseOnByte = false;
}
else if (g_debug_options.print_link_data && !skipPrint && key_held(KEY_SELECT))
{
pauseOnByte = false;
}
else if (g_debug_options.print_link_packets && !skipPrint && key_held(KEY_START))
{
pauseOnPacket = false;
}
else if ((g_debug_options.print_link_data || g_debug_options.print_link_packets) && key_held(KEY_UP))
{
skipPrint = true;
pauseOnPacket = false;
pauseOnByte = false;
}
else if ((g_debug_options.print_link_data || g_debug_options.print_link_packets) && key_held(KEY_DOWN))
{
skipPrint = false;
}
if (pauseOnByte && g_debug_options.print_link_data)
{
if (key_hit(KEY_B))
{
return false; // Even if paused, run once
}
}
if (pauseOnPacket && newPacket && g_debug_options.print_link_packets)
{
if (key_hit(KEY_A))
{
return false; // Even if paused, run once
}
}
return pauseOnByte || (pauseOnPacket && newPacket);
}
void LinkConnection::logicState_initConnection()
{
outData = 0x00;
switch (subState)
{
case CLOCK:
if (inData == 0xFE)
{
nextSubState = SAVE_SUCCESS;
// outData defaults to 0x00
}
else
{
outData = 0x01;
}
break;
case SAVE_SUCCESS:
if (inData == 0x60 || inData == 0x61)
{
nextSubState = MENU_OPEN;
outData = inData;
}
// outData defaults to 0x00
break;
case MENU_OPEN:
if (inData == 0xD0 || inData == 0x61)
{
if (inData == 0xD0)
{
gen = 1;
load_payload(GB_PayloadsFiles::UNIVERSALPAYLOADGEN1);
outData = 0xD4;
}
else if (inData == 0x61)
{
gen = 2;
load_payload(GB_PayloadsFiles::UNIVERSALPAYLOADGEN2);
outData = 0x61;
}
nextSubState = MENU_SUCCESS;
}
break;
case MENU_SUCCESS:
if (inData == 0xFE)
{
nextSubState = WAIT_FOR_TRADE;
}
outData = inData;
break;
case WAIT_FOR_TRADE:
if (inData == 0xFD)
{
REG_TM3D = -0x0040;
nextSubState = TRADE_PREAMBLE;
// outData defaults to 0x00
}
else
{
outData = inData;
}
break;
case TRADE_PREAMBLE:
if (subStateCounter < 2)
{
// outData defaults to 0x00
}
else if (subStateCounter < 9)
{
outData = 0xFD;
}
else
{
nextSubState = TRADE;
outData = 0xFD;
};
break;
case TRADE:
if (subStateCounter > curr_payload_size)
{
if (this->gen == 2)
{
nextSubState = MAIL;
}
else
{
nextSubState = WAIT_FOR_CHECKSUM_PAYLOAD;
}
}
outData = payloadBuffer[subStateCounter];
break;
case MAIL:
if (subStateCounter > 0x186)
{
nextSubState = WAIT_FOR_CHECKSUM_PAYLOAD;
}
outData = 0x00;
break;
case WAIT_FOR_CHECKSUM_PAYLOAD:
if (inData == 0xFD)
{
nextSubState = GET_CHECKSUM;
}
// outData defaults to 0x00
break;
case GET_CHECKSUM:
if (inData != 0xFD)
{
dataOutBuffer[dataOutBufferCurrIndex] = inData;
dataOutBufferCurrIndex++;
outData = 0x01;
}
else if (inData == 0xFD && dataOutBufferCurrIndex > 0)
{
loadCurrGameFromChecksum();
load_payload(GB_PayloadsFiles::SPECIFICPAYLOADGEN1_EN_R);
nextSubState = SEND_SPECIFIC_PAYLOAD;
}
else
{
outData = 0xFD;
}
break;
case WAIT_FOR_SECOND_PAYLOAD:
if (inData == 0xFD)
{
nextSubState = SEND_SPECIFIC_PAYLOAD;
}
// outData defaults to 0x00
break;
case SEND_SPECIFIC_PAYLOAD:
if (subStateCounter > 255) // The 255 comes from the Universal Payload
{
nextSubState = END;
}
if (subStateCounter < curr_payload_size)
{
outData = payloadBuffer[subStateCounter];
}
else
{
outData = 0x01;
}
break;
case END:
irq_disable(II_TIMER3);
break;
default:
outData = inData;
break;
}
}
void LinkConnection::logicState_packetExchange()
{
switch (subState)
{
case BYTE_EXCHANGE:
if (subStateCounter < TOTAL_PACKET_LENGTH)
{
// Start with OUT_PACKET_LENGTH of bytes of prep to make sure things are set to go
outData = 0xFF;
}
else
{
switch (subStateCounter % TOTAL_PACKET_LENGTH)
{
case 0:
outData = 0xFD;
break;
case 1:
outData = currLinkPacketArrIndex;
break;
case 2:
outData = currLinkPacketArr[currLinkPacketArrIndex].command;
break;
case 3:
outData = currLinkPacketArr[currLinkPacketArrIndex].argument[0];
break;
case 4:
outData = currLinkPacketArr[currLinkPacketArrIndex].argument[1];
break;
case 5:
outData = currLinkPacketArr[currLinkPacketArrIndex].pointer >> 0;
break;
case 6:
outData = currLinkPacketArr[currLinkPacketArrIndex].pointer >> 8;
break;
case TOTAL_PACKET_LENGTH - 1:
currLinkPacketArrIndex++;
default:
outData = 0xFF;
break;
}
if (currLinkPacketArrIndex >= currLinkPacketArrNum)
{
nextSubState = END;
}
if (subStateCounter % TOTAL_PACKET_LENGTH == 0)
{
newPacket = true;
processPacket();
}
else
{
newPacket = false;
}
dataOutBuffer[subStateCounter % TOTAL_PACKET_LENGTH] = inData;
}
break;
case END:
irq_disable(II_TIMER3);
break;
default:
outData = inData;
break;
}
}
void LinkConnection::load_payload(GB_PayloadsFiles payload)
{
u32 fileSize;
u8 decompressionBuffer[0x1000];
const u8 *chunkList[] = {(const u8 *)GB_Payloads_chunk0_lz10_bin};
FileContainerReader reader(chunkList, 1);
const u32 fileIndex = (u32)payload;
reader.init(decompressionBuffer, sizeof(decompressionBuffer));
fileSize = reader.getFileSize(fileIndex);
reader.seekToFile(fileIndex);
reader.read(this->payloadBuffer, fileSize);
this->curr_payload_size = fileSize;
}
void LinkConnection::loadCurrGameFromChecksum()
{
if (((dataOutBuffer[0] + dataOutBuffer[1]) & 0x7F) != dataOutBuffer[3])
{
currROM = GB_ROM_ERROR;
};
int start = RED_JP_v0;
int end = GOLD_JP_v0;
if (gen == 2)
{
start = end;
end = NO_GB_ROM;
}
for (int i = start; i < end; i++)
{
if (dataOutBuffer[0] == GameBoyROMChecksumTable[i][1] && dataOutBuffer[1] == GameBoyROMChecksumTable[i][2])
{
currROM = (GameBoyROM)GameBoyROMChecksumTable[i][3];
return;
}
}
currROM = GB_ROM_ERROR;
return;
}
bool LinkConnection::processPacket()
{
int checksum = 0;
LinkPacket &currPacket = currLinkPacketArr[dataOutBuffer[INP_COUNTER_INDEX]];
for (int i = INP_DELAY_FROM_OUTP; i < INP_LENGTH; i++)
{
if (i != INP_CHECKSUM_INDEX)
{
checksum += dataOutBuffer[i];
}
}
// Add the read pointer
checksum += ((currPacket.pointer + 8) >> 0) & 0xFF;
checksum += ((currPacket.pointer + 8) >> 8) & 0xFF;
checksum &= 0x7F;
byte lsbByte = dataOutBuffer[INP_LSB_INDEX] | dataOutBuffer[INP_LSB_INDEX + 1];
for (int i = 0; i < 8; i++)
{
currPacket.recievedData[i] =
(dataOutBuffer[INP_DATA_INDEX + i] << 1) | ((lsbByte >> (7 - i)) & 0b1);
}
if (checksum != dataOutBuffer[INP_CHECKSUM_INDEX])
{
currPacket.latestError = CHECKSUM_MISMATCH;
return false;
}
return true;
}