Battle cable support

work in progress
f355 and aero dancing seem to work
vonot and tetris don't
maxspeed is broken
This commit is contained in:
Flyinghead 2023-11-23 16:40:20 +01:00
parent a9d5b77053
commit 4efacd7d71
19 changed files with 920 additions and 210 deletions

View File

@ -155,6 +155,7 @@ Option<bool> GGPOChatTimeoutToggle("GGPOChatTimeoutToggle", true, "network");
Option<int> GGPOChatTimeout("GGPOChatTimeout", 10, "network");
Option<bool> NetworkOutput("NetworkOutput", false, "network");
Option<int> MultiboardSlaves("MultiboardSlaves", 1, "network");
Option<bool> BattleCableEnable("BattleCable", false, "network");
#ifdef SUPPORT_DISPMANX
Option<bool> DispmanxMaintainAspect("maintain_aspect", true, "dispmanx");

View File

@ -521,6 +521,7 @@ extern Option<bool> GGPOChatTimeoutToggle;
extern Option<int> GGPOChatTimeout;
extern Option<bool> NetworkOutput;
extern Option<int> MultiboardSlaves;
extern Option<bool> BattleCableEnable;
#ifdef SUPPORT_DISPMANX
extern Option<bool> DispmanxMaintainAspect;

View File

@ -834,6 +834,7 @@ void Emulator::start()
Get_Sh4Interpreter(&sh4_cpu);
INFO_LOG(DYNAREC, "Using Interpreter");
}
setupPtyPipe();
memwatch::protect();

View File

@ -300,6 +300,7 @@ public:
public:
// Serial TX
virtual void write(u8 data) { }
virtual void sendBreak() { }
// RX buffer Size
virtual int available() { return 0; }
// Serial RX
@ -310,6 +311,9 @@ public:
virtual void setPipe(Pipe *pipe) = 0;
virtual void updateStatus() = 0;
virtual void receiveBreak() {
die("unsupported");
}
virtual ~SerialPort() = default;
};

View File

@ -1287,6 +1287,7 @@ void init()
void term()
{
SCIFSerialPort::Instance().setPipe(nullptr);
delete hopper;
hopper = nullptr;
}

View File

@ -45,6 +45,7 @@ public:
~TouchscreenPipe()
{
SCIFSerialPort::Instance().setPipe(nullptr);
sh4_sched_unregister(schedId);
}

View File

@ -1,5 +1,6 @@
#pragma once
#include "hw/hwreg.h"
#include <deque>
extern u32 UBC[9];
extern u32 BSC[19];
@ -99,16 +100,47 @@ public:
void setPipe(Pipe *pipe) override {
this->pipe = pipe;
}
void updateStatus() override;
Pipe *getPipe() const {
return pipe;
}
void updateStatus() override {}
void receiveBreak() override;
void init();
void term();
void serialize(Serializer& ser);
void deserialize(Deserializer& deser);
static u8 readData(u32 addr);
static void writeData(u32 addr, u8 data);
static u16 readStatus(u32 addr);
static void writeStatus(u32 addr, u16 data);
static u16 readSCFDR2(u32 addr);
u8 SCFRDR2_read();
void SCFTDR2_write(u8 data);
u16 readStatus();
void writeStatus(u16 data);
u16 SCFDR2_read();
static u16 SCFCR2_read(u32 addr);
void SCFCR2_write(u16 data);
void SCSPTR2_write(u16 data);
static void SCBRR2_write(u32 addr, u8 data);
static void SCSMR2_write(u32 addr, u16 data);
static void SCSCR2_write(u32 addr, u16 data);
static SCIFSerialPort& Instance();
private:
void updateBaudRate();
void setBreak(bool on);
void sendBreak();
bool txDone();
void rxSched();
static int schedCallback(int tag, int cycles, int lag, void *arg);
Pipe *pipe = nullptr;
int schedId = -1;
int brkSchedId = -1;
int frameSize = 10; // default 8 data bits, 1 stop bit, no parity
int cyclesPerBit = SH4_MAIN_CLOCK / 6103;
u16 statusLastRead = 0;
std::deque<u8> txFifo;
std::deque<u8> rxFifo;
bool transmitting = false;
};
void setupPtyPipe();

View File

@ -1,6 +1,5 @@
/*
Dreamcast serial port.
This is missing most of the functionality, but works for KOS (And thats all that uses it)
*/
#include <cerrno>
#include <cstdlib>
@ -17,6 +16,16 @@
#include "hw/sh4/sh4_interrupts.h"
#include "cfg/option.h"
#include "modules.h"
#include "hw/sh4/sh4_sched.h"
#include "serialize.h"
//#define DEBUG_SCIF
#ifdef DEBUG_SCIF
#define SCIF_LOG(...) INFO_LOG(SH4, __VA_ARGS__)
#else
#define SCIF_LOG(...)
#endif
SCIRegisters sci;
SCIFRegisters scif;
@ -28,79 +37,191 @@ static void updateInterrupts()
InterruptPend(sh4_SCIF_RXI, SCIF_SCFSR2.RDF || SCIF_SCFSR2.DR);
InterruptMask(sh4_SCIF_RXI, SCIF_SCSCR2.RIE);
InterruptPend(sh4_SCIF_BRI, SCIF_SCFSR2.BRK);
InterruptMask(sh4_SCIF_BRI, SCIF_SCSCR2.RIE || SCIF_SCSCR2.REIE);
InterruptPend(sh4_SCIF_ERI, SCIF_SCFSR2.ER || SCIF_SCFSR2.FER || SCIF_SCFSR2.PER);
InterruptMask(sh4_SCIF_ERI, SCIF_SCSCR2.RIE || SCIF_SCSCR2.REIE);
}
// SCIF SCFTDR2
void SCIFSerialPort::writeData(u32 addr, u8 data)
int SCIFSerialPort::schedCallback(int tag, int cycles, int lag, void *arg)
{
//DEBUG_LOG(COMMON, "serial out %02x %c", data, data);
if (Instance().pipe != nullptr)
Instance().pipe->write(data);
SCIF_SCFSR2.TDFE = 1;
SCIF_SCFSR2.TEND = 1;
updateInterrupts();
}
// SCIF_SCFSR2 read
u16 SCIFSerialPort::readStatus(u32 addr)
{
Instance().updateStatus();
return SCIF_SCFSR2.full;
}
// SCIF_SCFSR2 write
void SCIFSerialPort::writeStatus(u32 addr, u16 data)
{
if (!SCIF_SCFSR2.BRK)
data &= ~0x10;
SCIF_SCFSR2.full = data & 0x00f3;
SCIF_SCFSR2.TDFE = 1;
SCIF_SCFSR2.TEND = 1;
Instance().updateStatus();
}
//SCIF_SCFDR2 - 16b
u16 SCIFSerialPort::readSCFDR2(u32 addr)
{
if (Instance().pipe != nullptr)
return std::min(16, Instance().pipe->available());
else
return 0;
}
//SCIF_SCFRDR2
u8 SCIFSerialPort::readData(u32 addr)
{
u8 data = 0;
if (Instance().pipe != nullptr) {
data = Instance().pipe->read();
//DEBUG_LOG(COMMON, "serial in %02x %c", data, data);
SCIFSerialPort& scif = *(SCIFSerialPort *)arg;
if (tag == 0)
{
bool reschedule = scif.txDone();
scif.rxSched();
if (reschedule || scif.pipe != nullptr)
return scif.frameSize * scif.cyclesPerBit;
else
return 0;
}
else
{
scif.sendBreak();
return 0;
}
Instance().updateStatus();
return data;
}
void SCIFSerialPort::updateStatus()
bool SCIFSerialPort::txDone()
{
if (!transmitting || SCIF_SCFCR2.TFRST == 1)
return false;
if (txFifo.empty())
{
SCIF_SCFSR2.TEND = 1;
SCIF_SCFSR2.TDFE = 1; // should not be set since the tx fifo hasn't changed but vonot needs it
updateInterrupts();
transmitting = false;
return false; // don't reschedule
}
u8 v = txFifo.front();
txFifo.pop_front();
if (pipe != nullptr)
pipe->write(v);
u32 txTrigger = 1 << (3 - SCIF_SCFCR2.TTRG);
if (txFifo.size() <= txTrigger) {
SCIF_SCFSR2.TDFE = 1;
updateInterrupts();
}
return true;
}
void SCIFSerialPort::rxSched()
{
if (pipe == nullptr)
return;
constexpr int trigLevels[] { 1, 4, 8, 14 };
int avail = pipe->available();
if (avail >= trigLevels[SCIF_SCFCR2.RTRG1 * 2 + SCIF_SCFCR2.RTRG0])
// FIXME fifo size checked to avoid overruns but incorrect
if (rxFifo.size() >= 16)
{
SCIF_SCFSR2.RDF = 1;
if (avail >= 1)
updateInterrupts();
return;
}
if (pipe->available() > 0)
{
u8 v = pipe->read();
if (SCIF_SCSCR2.RE == 0 || SCIF_SCFCR2.RFRST == 1)
return;
if (rxFifo.size() == 16)
{
// rx overrun
SCIF_SCLSR2.ORER = 1;
updateInterrupts();
WARN_LOG(SH4, "scif: Receive overrun");
}
else
{
rxFifo.push_back(v);
constexpr u32 trigLevels[] { 1, 4, 8, 14 };
if (rxFifo.size() >= trigLevels[SCIF_SCFCR2.RTRG]) {
SCIF_SCFSR2.RDF = 1;
updateInterrupts();
}
}
}
// TODO fifo might have been emptied since last rx
else if (!rxFifo.empty())
{
SCIF_SCFSR2.DR = 1;
updateInterrupts();
}
}
void SCIFSerialPort::updateBaudRate()
{
// 1 start bit, 7 or 8 data bits, optional parity bit, 1 or 2 stop bits
frameSize = 1 + 8 - SCIF_SCSMR2.CHR + SCIF_SCSMR2.PE + 1 + SCIF_SCSMR2.STOP;
int bauds = SH4_MAIN_CLOCK / 4 / (SCIF_SCBRR2 + 1) / 32 / (1 << (SCIF_SCSMR2.CKS * 2));
cyclesPerBit = SH4_MAIN_CLOCK / bauds;
INFO_LOG(SH4, "SCIF: Frame size %d cycles/bit %d (%d bauds)", frameSize, cyclesPerBit, bauds);
if (sh4_sched_is_scheduled(schedId))
sh4_sched_request(schedId, frameSize * cyclesPerBit);
}
// SCIF SCFTDR2 - Transmit FIFO Data Register
void SCIFSerialPort::SCFTDR2_write(u8 data)
{
SCIF_LOG("serial out %02x %c fifo_sz %d", data, data == '\0' ? ' ' : data, (int)txFifo.size());
if (SCIF_SCFCR2.TFRST == 1)
return;
if (SCIF_SCSMR2.CHR == 1)
data &= 0x7f;
if (txFifo.empty() && !transmitting && SCIF_SCSCR2.TE == 1)
{
if (pipe != nullptr)
pipe->write(data);
transmitting = true;
// Need to reschedule so it's doesn't happen too early (f355)
sh4_sched_request(schedId, frameSize * cyclesPerBit);
SCIF_SCFSR2.TDFE = 1; // immediately transfer SCFTDR2 into the shift register
updateInterrupts();
}
else if (txFifo.size() < 16) {
txFifo.push_back(data);
}
}
// SCIF_SCFSR2 read - Serial Status Register
u16 SCIFSerialPort::readStatus()
{
// SCIF_LOG("SCIF_SCFSR2.read %s%s%s%s%s%s%s%s",
// SCIF_SCFSR2.ER ? "ER " : "",
// SCIF_SCFSR2.TEND ? "TEND " : "",
// SCIF_SCFSR2.TDFE ? "TDFE " : "",
// SCIF_SCFSR2.BRK ? "BRK " : "",
// SCIF_SCFSR2.FER ? "FER " : "",
// SCIF_SCFSR2.PER ? "PER " : "",
// SCIF_SCFSR2.RDF ? "RDF " : "",
// SCIF_SCFSR2.DR ? "DR" : "");
statusLastRead = SCIF_SCFSR2.full;
return SCIF_SCFSR2.full;
}
// SCIF_SCFSR2 write - Serial Status Register
void SCIFSerialPort::writeStatus(u16 data)
{
data = data | ~0x00f3 | ~statusLastRead;
SCIF_LOG("SCIF_SCFSR2.reset %s%s%s%s%s%s%s%s",
(data & 0x80) ? "" : "ER ",
(data & 0x40) ? "" : "TEND ",
(data & 0x20) ? "" : "TDFE ",
(data & 0x10) ? "" : "BRK ",
(data & 0x08) ? "" : "FER ",
(data & 0x04) ? "" : "PER ",
(data & 0x02) ? "" : "RDF ",
(data & 0x01) ? "" : "DR");
SCIF_SCFSR2.full &= data;
statusLastRead &= data;
updateInterrupts();
}
//SCIF_SCFDR2 - FIFO Data Count Register
u16 SCIFSerialPort::SCFDR2_read()
{
u16 rv = rxFifo.size() | (txFifo.size() << 8);
SCIF_LOG("SCIF: fifo count rx %d tx %d", rv & 0xff, rv >> 8);
return rv;
}
//SCIF_SCFRDR2 - Receive FIFO Data Register
u8 SCIFSerialPort::SCFRDR2_read()
{
if (rxFifo.empty()) {
WARN_LOG(SH4, "Empty rx fifo read");
return 0;
}
u8 data = rxFifo.front();
rxFifo.pop_front();
SCIF_LOG("serial in %02x %c", data, data);
return data;
}
SCIFSerialPort& SCIFSerialPort::Instance()
{
static SCIFSerialPort instance;
@ -108,25 +229,210 @@ SCIFSerialPort& SCIFSerialPort::Instance()
return instance;
}
//SCSCR2
//SCSCR2 - Serial Control Register
static u16 SCSCR2_read(u32 addr)
{
return SCIF_SCSCR2.full;
}
static void SCSCR2_write(u32 addr, u16 data)
void SCIFSerialPort::SCSCR2_write(u32 addr, u16 data)
{
SCIF_SCSCR2.full = data & 0x00fa;
if (SCIF_SCSCR2.TE == 0)
{
SCIF_SCFSR2.TEND = 1;
//SCIF_SCFSR2.TDFE = 1; // TODO not sure about this one
// TE must be cleared to send a break
Instance().setBreak(SCIF_SCSPTR2.SPB2IO == 1 && SCIF_SCSPTR2.SPB2DT == 0);
}
else {
Instance().setBreak(false);
}
updateInterrupts();
SCIF_LOG("SCIF_SCSCR2= %s%s%s%s%s",
SCIF_SCSCR2.TIE ? "TIE " : "",
SCIF_SCSCR2.RIE ? "RIE " : "",
SCIF_SCSCR2.TE ? "TE " : "",
SCIF_SCSCR2.RE ? "RE " : "",
SCIF_SCSCR2.REIE ? "REIE" : "");
}
// SCSPTR2 - Serial Port Register
static u16 SCSPTR2_read(u32 addr)
{
SCIF_LOG("SCIF_SCSPTR2.read %x", SCIF_SCSPTR2.full);
return SCIF_SCSPTR2.full & ~0x10; // CTS active/low
}
void SCIFSerialPort::setBreak(bool on)
{
if (on) {
// tetris needs to send/receive breaks
if (!sh4_sched_is_scheduled(brkSchedId))
sh4_sched_request(brkSchedId, cyclesPerBit * frameSize);
}
else {
if (sh4_sched_is_scheduled(brkSchedId))
sh4_sched_request(brkSchedId, -1);
}
}
void SCIFSerialPort::SCSPTR2_write(u16 data)
{
SCIF_SCSPTR2.full = data & 0x00f3;
if (SCIF_SCSPTR2.SPB2IO == 1)
setBreak(SCIF_SCSPTR2.SPB2DT == 0 && SCIF_SCSCR2.TE == 0);
else
setBreak(false);
SCIF_LOG("SCIF_SCSPTR2= %s%s%s%s%s%s",
SCIF_SCSPTR2.RTSIO ? "RTSIO " : "",
SCIF_SCSPTR2.RTSDT ? "RTSDT " : "",
SCIF_SCSPTR2.CTSIO ? "CTSIO " : "",
SCIF_SCSPTR2.CTSDT ? "CTSDT " : "",
SCIF_SCSPTR2.SPB2IO ? "SPB2IO " : "",
SCIF_SCSPTR2.SPB2DT ? "SPB2DT" : "");
}
// SCFCR2 - FIFO Control Register
u16 SCIFSerialPort::SCFCR2_read(u32 addr)
{
// SCIF_LOG("SCIF_SCFCR2.read %x", SCIF_SCFCR2.full);
return SCIF_SCFCR2.full;
}
void SCIFSerialPort::SCFCR2_write(u16 data)
{
if (SCIF_SCFCR2.TFRST == 1 && !(data & 4))
{
// when TFRST 1 -> 0
// seems to help tetris send data during sync
SCIF_SCFSR2.TEND = 1;
SCIF_SCFSR2.TDFE = 1;
updateInterrupts();
}
SCIF_SCFCR2.full = data & 0x00ff;
if (SCIF_SCFCR2.TFRST == 1)
{
txFifo.clear();
if (pipe == nullptr)
sh4_sched_request(schedId, -1);
transmitting = false;
}
if (SCIF_SCFCR2.RFRST == 1)
rxFifo.clear();
SCIF_LOG("SCIF_SCFCR2= %s%s%sTTRG %d RTRG %d",
SCIF_SCFCR2.RFRST ? "RFRST " : "",
SCIF_SCFCR2.TFRST ? "TFRST " : "",
SCIF_SCFCR2.MCE ? "MCE " : "",
SCIF_SCFCR2.TTRG,
SCIF_SCFCR2.RTRG);
}
// SCBRR2 - Bit Rate Register
void SCIFSerialPort::SCBRR2_write(u32 addr, u8 data)
{
SCIF_SCBRR2 = data;
Instance().updateBaudRate();
}
// SCSMR2 - Serial Mode Register
void SCIFSerialPort::SCSMR2_write(u32 addr, u16 data)
{
SCIF_SCSMR2.full = data & 0x007b;
Instance().updateBaudRate();
}
void SCIFSerialPort::receiveBreak()
{
SCIF_LOG("Break received");
SCIF_SCFSR2.BRK = 1;
updateInterrupts();
}
void SCIFSerialPort::sendBreak()
{
if (pipe != nullptr)
pipe->sendBreak();
}
void SCIFSerialPort::init()
{
if (schedId == -1)
schedId = sh4_sched_register(0, schedCallback, this);
if (brkSchedId == -1)
brkSchedId = sh4_sched_register(1, schedCallback, this);
}
void SCIFSerialPort::term()
{
if (schedId != -1) {
sh4_sched_unregister(schedId);
schedId = -1;
}
if (brkSchedId != -1) {
sh4_sched_unregister(brkSchedId);
brkSchedId = -1;
}
}
void SCIFSerialPort::serialize(Serializer& ser)
{
sh4_sched_serialize(ser, schedId);
sh4_sched_serialize(ser, brkSchedId);
ser << statusLastRead;
ser << (int)txFifo.size();
for (u8 b : txFifo)
ser << b;
ser << (int)rxFifo.size();
for (u8 b : txFifo)
ser << b;
ser << transmitting;
}
void SCIFSerialPort::deserialize(Deserializer& deser)
{
txFifo.clear();
rxFifo.clear();
if (deser.version() >= Deserializer::V43)
{
sh4_sched_deserialize(deser, schedId);
sh4_sched_deserialize(deser, brkSchedId);
deser >> statusLastRead;
int size;
deser >> size;
for (int i = 0; i < size; i++)
{
u8 b;
deser >> b;
txFifo.push_back(b);
}
deser >> size;
for (int i = 0; i < size; i++)
{
u8 b;
deser >> b;
rxFifo.push_back(b);
}
deser >> transmitting;
}
else
{
statusLastRead = 0;
transmitting = false;
}
updateBaudRate();
}
struct PTYPipe : public SerialPort::Pipe
{
void write(u8 data) override {
if (config::SerialConsole)
::write(tty, &data, 1);
void write(u8 data) override
{
if (config::SerialConsole) {
int rc = ::write(tty, &data, 1);
(void)rc;
}
}
int available() override {
@ -138,10 +444,13 @@ struct PTYPipe : public SerialPort::Pipe
return count;
}
u8 read() override {
u8 read() override
{
u8 data = 0;
if (tty != 1)
::read(tty, &data, 1);
if (tty != 1) {
int rc = ::read(tty, &data, 1);
(void)rc;
}
return data;
}
@ -189,12 +498,46 @@ struct PTYPipe : public SerialPort::Pipe
::close(tty);
tty = 1;
}
SCIFSerialPort::Instance().setPipe(nullptr);
}
private:
int tty = 1;
};
static PTYPipe ptyPipe;
void setupPtyPipe()
{
static PTYPipe ptyPipe;
if (config::SerialConsole || config::SerialPTY)
{
if (SCIFSerialPort::Instance().getPipe() == nullptr)
ptyPipe.init();
}
else
{
if (SCIFSerialPort::Instance().getPipe() == &ptyPipe)
ptyPipe.term();
}
}
template <typename T>
class SingletonForward {
};
template<typename Ret, typename Class, typename... Args>
struct SingletonForward<Ret(Class::*)(Args...)>
{
static Ret(*forward(Ret(*function)(u32 addr, Args...)))(u32 addr, Args...) {
return function;
}
};
#define SINGLETON_FORWARD(accessor, function) \
SingletonForward<decltype(&std::remove_reference<decltype(accessor)>::type::function)>::forward([](u32 addr, auto... args) { \
return accessor.function(args...); \
})
//Init term res
void SCIFRegisters::init()
@ -204,56 +547,43 @@ void SCIFRegisters::init()
// Serial Communication Interface with FIFO
//SCIF SCSMR2 0xFFE80000 0x1FE80000 16 0x0000 0x0000 Held Held Pclk
setRW<SCIF_SCSMR2_addr, u16, 0x007b>();
setWriteHandler<SCIF_SCSMR2_addr, u16>(SCIFSerialPort::SCSMR2_write);
//SCIF SCBRR2 0xFFE80004 0x1FE80004 8 0xFF 0xFF Held Held Pclk
setRW<SCIF_SCBRR2_addr, u8>();
setWriteHandler<SCIF_SCBRR2_addr, u8>(SCIFSerialPort::SCBRR2_write);
//SCIF SCSCR2 0xFFE80008 0x1FE80008 16 0x0000 0x0000 Held Held Pclk
setHandlers<SCIF_SCSCR2_addr>(SCSCR2_read, SCSCR2_write);
setHandlers<SCIF_SCSCR2_addr>(SCSCR2_read, SCIFSerialPort::SCSCR2_write);
//SCIF SCFTDR2 0xFFE8000C 0x1FE8000C 8 Undefined Undefined Held Held Pclk
setWriteOnly<SCIF_SCFTDR2_addr>(SCIFSerialPort::writeData);
setWriteOnly<SCIF_SCFTDR2_addr>(SINGLETON_FORWARD(SCIFSerialPort::Instance(), SCFTDR2_write));
//SCIF SCFSR2 0xFFE80010 0x1FE80010 16 0x0060 0x0060 Held Held Pclk
setHandlers<SCIF_SCFSR2_addr>(SCIFSerialPort::readStatus, SCIFSerialPort::writeStatus);
setHandlers<SCIF_SCFSR2_addr>(SINGLETON_FORWARD(SCIFSerialPort::Instance(), readStatus),
SINGLETON_FORWARD(SCIFSerialPort::Instance(), writeStatus));
//READ only
//SCIF SCFRDR2 0xFFE80014 0x1FE80014 8 Undefined Undefined Held Held Pclk
setReadOnly<SCIF_SCFRDR2_addr>(SCIFSerialPort::readData);
setReadOnly<SCIF_SCFRDR2_addr>(SINGLETON_FORWARD(SCIFSerialPort::Instance(), SCFRDR2_read));
//SCIF SCFCR2 0xFFE80018 0x1FE80018 16 0x0000 0x0000 Held Held Pclk
setRW<SCIF_SCFCR2_addr, u16, 0x00ff>();
setHandlers<SCIF_SCFCR2_addr>(SCIFSerialPort::SCFCR2_read, SINGLETON_FORWARD(SCIFSerialPort::Instance(), SCFCR2_write));
//Read only
//SCIF SCFDR2 0xFFE8001C 0x1FE8001C 16 0x0000 0x0000 Held Held Pclk
setReadOnly<SCIF_SCFDR2_addr>(SCIFSerialPort::readSCFDR2);
setReadOnly<SCIF_SCFDR2_addr>(SINGLETON_FORWARD(SCIFSerialPort::Instance(), SCFDR2_read));
//SCIF SCSPTR2 0xFFE80020 0x1FE80020 16 0x0000 0x0000 Held Held Pclk
setRW<SCIF_SCSPTR2_addr, u16, 0x00f3>();
setHandlers<SCIF_SCSPTR2_addr>(SCSPTR2_read, SINGLETON_FORWARD(SCIFSerialPort::Instance(), SCSPTR2_write));
//SCIF SCLSR2 0xFFE80024 0x1FE80024 16 0x0000 0x0000 Held Held Pclk
setRW<SCIF_SCLSR2_addr, u16, 1>();
SCIFSerialPort::Instance().init();
reset(true);
}
void SCIRegisters::init()
{
super::init();
// Serial Communication Interface
setRW<SCI_SCSMR1_addr, u8>();
setRW<SCI_SCBRR1_addr, u8>();
setRW<SCI_SCSCR1_addr, u8>();
setRW<SCI_SCTDR1_addr, u8>();
setRW<SCI_SCSSR1_addr, u8, 0xf9>();
setReadOnly<SCI_SCRDR1_addr, u8>();
setRW<SCI_SCSPTR1_addr, u8, 0x8f>();
reset();
}
void SCIFRegisters::reset(bool hard)
{
super::reset();
@ -274,7 +604,30 @@ void SCIFRegisters::reset(bool hard)
SCIF_SCFSR2.full = 0x060;
if (hard)
ptyPipe.init();
SCIFSerialPort::Instance().setPipe(nullptr);
}
void SCIFRegisters::term()
{
SCIFSerialPort::Instance().term();
super::term();
}
void SCIRegisters::init()
{
super::init();
// Serial Communication Interface
setRW<SCI_SCSMR1_addr, u8>();
setRW<SCI_SCBRR1_addr, u8>();
setRW<SCI_SCSCR1_addr, u8>();
setRW<SCI_SCTDR1_addr, u8>();
setRW<SCI_SCSSR1_addr, u8, 0xf9>();
setReadOnly<SCI_SCRDR1_addr, u8>();
setRW<SCI_SCSPTR1_addr, u8, 0x8f>();
reset();
}
void SCIRegisters::reset()
@ -285,9 +638,3 @@ void SCIRegisters::reset()
SCI_SCTDR1 = 0xff;
SCI_SCSSR1 = 0x84;
}
void SCIFRegisters::term()
{
super::term();
ptyPipe.term();
}

View File

@ -684,6 +684,7 @@ void serialize(Serializer& ser)
ser << TMU;
ser << SCI;
ser << SCIF;
SCIFSerialPort::Instance().serialize(ser);
icache.Serialize(ser);
ocache.Serialize(ser);
@ -739,6 +740,7 @@ void deserialize(Deserializer& deser)
deser >> SCI;
deser >> SCIF;
}
SCIFSerialPort::Instance().deserialize(deser);
if (deser.version() >= Deserializer::V9
// Note (lr): was added in V11 fa49de29 24/12/2020 but ver not updated until V12 (13/4/2021)
|| (deser.version() >= Deserializer::V11_LIBRETRO && deser.version() <= Deserializer::VLAST_LIBRETRO))

View File

@ -1233,23 +1233,13 @@ union SCIF_SCSMR2_type
{
struct
{
u32 CKS0 : 1;
u32 CKS1 : 1;
u32 res_0 : 1;
u32 CKS : 2;
u32 : 1;
u32 STOP : 1;
u32 OE_paritymode : 1;
u32 PE : 1;
u32 CHR : 1;
u32 res_1 : 1;
//8
u32 res_2 : 1;
u32 res_3 : 1;
u32 res_4 : 1;
u32 res_5 : 1;
u32 res_6 : 1;
u32 res_7 : 1;
u32 res_8 : 1;
u32 res_9 : 1;
u32 : 9;
//16
};
u16 full;
@ -1265,23 +1255,16 @@ union SCIF_SCSCR2_type
{
struct
{
u32 res_0 : 1;
u32 : 1;
u32 CKE1 : 1;
u32 res_1 : 1;
u32 : 1;
u32 REIE : 1;
u32 RE : 1;
u32 TE : 1;
u32 RIE : 1;
u32 TIE : 1;
//8
u32 res_2 : 1;
u32 res_3 : 1;
u32 res_4 : 1;
u32 res_5 : 1;
u32 res_6 : 1;
u32 res_7 : 1;
u32 res_8 : 1;
u32 res_9 : 1;
u32 : 8;
//16
};
u16 full;
@ -1305,14 +1288,8 @@ union SCIF_SCFSR2_type
u32 TEND : 1;
u32 ER : 1;
//8
u32 FER0 : 1;
u32 FER1 : 1;
u32 FER2 : 1;
u32 FER3 : 1;
u32 PER0 : 1;
u32 PER1 : 1;
u32 PER2 : 1;
u32 PER3 : 1;
u32 FERn : 4;
u32 PERn : 4;
//16
};
u16 full;
@ -1331,19 +1308,10 @@ union SCIF_SCFCR2_type
u32 RFRST : 1;
u32 TFRST : 1;
u32 MCE : 1;
u32 TTRG0 : 1;
u32 TTRG1 : 1;
u32 RTRG0 : 1;
u32 RTRG1 : 1;
u32 TTRG : 2;
u32 RTRG : 2;
//8
u32 res_0 : 1;
u32 res_1 : 1;
u32 res_2 : 1;
u32 res_3 : 1;
u32 res_4 : 1;
u32 res_5 : 1;
u32 res_6 : 1;
u32 res_7 : 1;
u32 : 8;
//16
};
u16 full;
@ -1357,10 +1325,10 @@ union SCIF_SCFDR2_type
struct
{
u32 R : 5;
u32 res_0 : 3;
u32 : 3;
//8
u32 T : 5;
u32 res_1 : 3;
u32 : 3;
//16
};
u16 full;
@ -1373,21 +1341,13 @@ union SCIF_SCSPTR2_type
{
u32 SPB2DT : 1;
u32 SPB2IO : 1;
u32 res_0 : 1;
u32 res_1 : 1;
u32 : 2;
u32 CTSDT : 1;
u32 CTSIO : 1;
u32 RTSDT : 1;
u32 RTSIO : 1;
//8
u32 res_2 : 1;
u32 res_3 : 1;
u32 res_4 : 1;
u32 res_5 : 1;
u32 res_6 : 1;
u32 res_7 : 1;
u32 res_8 : 1;
u32 res_9 : 1;
u32 : 8;
//16
};
u16 full;
@ -1400,9 +1360,7 @@ union SCIF_SCLSR2_type
struct
{
u32 ORER : 1;
u32 res_0 : 7;
//8
u32 res_1 : 8;
u32 :15;
//16
};
u16 full;

View File

@ -138,6 +138,11 @@ void sh4_sched_request(int id, int cycles)
sh4_sched_ffts();
}
bool sh4_sched_is_scheduled(int id)
{
return sch_list[id].end != -1;
}
/* Returns how much time has passed for this callback */
static int sh4_sched_elapsed(sched_list& sched)
{

View File

@ -31,11 +31,16 @@ u64 sh4_sched_now64();
Schedule a callback to be called sh4 *cycles* after the
invocation of this function. *Cycles* range is (0, 200M].
Passing a value of 0 disables the callback.
Passing a value of -1 disables the callback.
If called multiple times, only the last call is in effect
*/
void sh4_sched_request(int id, int cycles);
/*
Returns true if the callback is scheduled to be called in the future.
*/
bool sh4_sched_is_scheduled(int id);
/*
Tick for *cycles*
*/

View File

@ -20,7 +20,9 @@
#include "cfg/option.h"
#include "ggpo.h"
#include "naomi_network.h"
#include "net_serial_maxspeed.h"
//#include "net_serial_maxspeed.h"
#include "null_modem.h"
#include "hw/naomi/naomi_flashrom.h"
NetworkHandshake *NetworkHandshake::instance;
@ -69,7 +71,13 @@ void NetworkHandshake::init()
else if (NaomiNetworkSupported())
instance = new NaomiNetworkHandshake();
else if (config::NetworkEnable && settings.content.gameId == "MAXIMUM SPEED")
instance = new MaxSpeedHandshake();
// instance = new MaxSpeedHandshake();
{
configure_maxspeed_flash(true, config::ActAsServer);
instance = new BattleCableHandshake();
}
else if (config::BattleCableEnable && !settings.platform.isNaomi())
instance = new BattleCableHandshake();
else
instance = nullptr;
}

View File

@ -88,12 +88,14 @@ struct MaxSpeedNetPipe : public SerialPort::Pipe
}
createSocket();
SCIFSerialPort::Instance().setPipe(this);
return true;
}
void shutdown()
{
SCIFSerialPort::Instance().setPipe(nullptr);
enableNetworkBroadcast(false);
if (VALID(sock))
closesocket(sock);
@ -215,7 +217,8 @@ private:
peerAddress.sin_family = AF_INET;
peerAddress.sin_addr.s_addr = INADDR_BROADCAST;
peerAddress.sin_port = htons(NaomiNetwork::SERVER_PORT);
if (!config::NetworkServer.get().empty())
// ignore server name if acting as server
if (!config::NetworkServer.get().empty() && !config::ActAsServer)
{
auto pos = config::NetworkServer.get().find_last_of(':');
std::string server;
@ -242,8 +245,6 @@ private:
}
else
enableNetworkBroadcast(true);
SCIFSerialPort::Instance().setPipe(this);
}
sock_t sock = INVALID_SOCKET;

271
core/network/null_modem.h Normal file
View File

@ -0,0 +1,271 @@
/*
Copyright 2023 flyinghead
This file is part of Flycast.
Flycast is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Flycast is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#include "hw/sh4/modules/modules.h"
#include "net_platform.h"
#include "cfg/option.h"
#include "miniupnp.h"
#include "hw/sh4/sh4_sched.h"
#include "naomi_network.h"
#include "net_handshake.h"
#include <deque>
class NullModemPipe : public SerialPort::Pipe
{
public:
class Exception : public FlycastException
{
public:
Exception(const std::string& reason) : FlycastException(reason) {}
};
// Serial TX
void write(u8 data) override
{
u8 packet[2] = { 'D', data };
int rc = sendto(sock, (const char *)&packet[0], sizeof(packet), 0, (const sockaddr *)&peerAddress, sizeof(peerAddress));
if (rc != sizeof(packet))
ERROR_LOG(NETWORK, "sendto: %d errno %d", rc, get_last_error());
DEBUG_LOG(NETWORK, "Write %02x %c (buf rx %d)", data, data, (int)rxBuffer.size());
}
void sendBreak() override
{
const char b = 'B';
int rc = sendto(sock, &b, 1, 0, (const sockaddr *)&peerAddress, sizeof(peerAddress));
if (rc != 1)
ERROR_LOG(NETWORK, "sendto: %d errno %d", rc, get_last_error());
DEBUG_LOG(NETWORK, "Send Break");
}
// RX buffer Size
int available() override {
poll();
checkBreak();
int realSize = 0;
for (u32 b : rxBuffer)
if (b != (u32)~0)
realSize++;
return realSize;
}
// Serial RX
u8 read() override
{
poll();
if (rxBuffer.empty()) {
WARN_LOG(NETWORK, "NetPipe: empty read");
return 0;
}
u8 b = rxBuffer.front();
rxBuffer.pop_front();
DEBUG_LOG(NETWORK, "Read %02x (buf rx %d)", b, (int)rxBuffer.size());
checkBreak();
return b;
}
~NullModemPipe()
{
shutdown();
}
bool init()
{
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0)
{
ERROR_LOG(NETWORK, "WSAStartup failed. errno=%d", get_last_error());
throw Exception("WSAStartup failed");
}
#endif
if (config::EnableUPnP)
{
miniupnp.Init();
miniupnp.AddPortMapping(config::LocalPort, true);
}
createSocket();
SCIFSerialPort::Instance().setPipe(this);
return true;
}
void shutdown()
{
enableNetworkBroadcast(false);
if (VALID(sock))
closesocket(sock);
sock = INVALID_SOCKET;
SCIFSerialPort::Instance().setPipe(nullptr);
}
private:
void checkBreak()
{
if (!rxBuffer.empty() && rxBuffer.front() == (u32)~0) {
SCIFSerialPort::Instance().receiveBreak();
rxBuffer.pop_front();
}
}
void poll()
{
if (lastPoll == sh4_sched_now64())
return;
lastPoll = sh4_sched_now64();
u8 data[0x100];
sockaddr_in addr;
while (true)
{
socklen_t len = sizeof(addr);
int rc = recvfrom(sock, (char *)data, sizeof(data), 0, (sockaddr *)&addr, &len);
if (rc == -1)
{
int error = get_last_error();
if (error == L_EWOULDBLOCK || error == L_EAGAIN)
break;
#ifdef _WIN32
if (error == WSAECONNRESET)
// Happens if the previous send resulted in an ICMP Port Unreachable message
break;
#endif
throw Exception("Receive error: errno " + std::to_string(error));
}
if (peerAddress.sin_addr.s_addr == INADDR_BROADCAST)
{
if (addr.sin_port != htons(config::LocalPort) || !is_local_address(addr.sin_addr.s_addr))
{
peerAddress.sin_addr.s_addr = addr.sin_addr.s_addr;
peerAddress.sin_port = addr.sin_port;
enableNetworkBroadcast(false);
NOTICE_LOG(NETWORK, "Data received from peer %x:%d", htonl(addr.sin_addr.s_addr), htons(addr.sin_port));
}
else
{
// this is coming from us so ignore it
continue;
}
}
if (rc == 2)
{
if (data[0] != 'D')
ERROR_LOG(NETWORK, "Unexpected packet '%c'", data[0]);
else
rxBuffer.push_back(data[1]);
}
else if (rc == 1)
{
if (data[0] != 'B')
ERROR_LOG(NETWORK, "Unexpected packet '%c'", data[0]);
else
rxBuffer.push_back(~0);
}
}
}
void createSocket()
{
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock == INVALID_SOCKET)
{
ERROR_LOG(NETWORK, "Socket creation failed: errno %d", get_last_error());
throw Exception("Socket creation failed");
}
int option = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&option, sizeof(option));
sockaddr_in serveraddr{};
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(config::LocalPort);
if (::bind(sock, (sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
ERROR_LOG(NETWORK, "NaomiServer: bind() failed. errno=%d", get_last_error());
closesocket(sock);
throw Exception("Socket bind failed");
}
set_non_blocking(sock);
// Allow broadcast packets to be sent
int broadcast = 1;
if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (const char *)&broadcast, sizeof(broadcast)) == -1)
WARN_LOG(NETWORK, "setsockopt(SO_BROADCAST) failed. errno=%d", get_last_error());
peerAddress.sin_family = AF_INET;
peerAddress.sin_addr.s_addr = INADDR_BROADCAST;
peerAddress.sin_port = htons(NaomiNetwork::SERVER_PORT);
if (!config::NetworkServer.get().empty()
// ignore server name if acting as server (maxspeed)
&& (!config::ActAsServer || settings.platform.isConsole()))
{
auto pos = config::NetworkServer.get().find_last_of(':');
std::string server;
if (pos != std::string::npos)
{
peerAddress.sin_port = htons(atoi(config::NetworkServer.get().substr(pos + 1).c_str()));
server = config::NetworkServer.get().substr(0, pos);
}
else
server = config::NetworkServer;
addrinfo *resultAddr;
if (getaddrinfo(server.c_str(), 0, nullptr, &resultAddr))
WARN_LOG(NETWORK, "Server %s is unknown", server.c_str());
else
{
for (addrinfo *ptr = resultAddr; ptr != nullptr; ptr = ptr->ai_next)
if (ptr->ai_family == AF_INET)
{
peerAddress.sin_addr.s_addr = ((sockaddr_in *)ptr->ai_addr)->sin_addr.s_addr;
break;
}
freeaddrinfo(resultAddr);
}
}
else
enableNetworkBroadcast(true);
}
sock_t sock = INVALID_SOCKET;
MiniUPnP miniupnp;
std::deque<u32> rxBuffer;
sockaddr_in peerAddress{};
u64 lastPoll = 0;
};
class BattleCableHandshake : public NetworkHandshake
{
public:
std::future<bool> start() override {
std::promise<bool> promise;
promise.set_value(pipe.init());
return promise.get_future();
}
void stop() override {
pipe.shutdown();
}
bool canStartNow() override {
return true;
}
void startNow() override {}
private:
NullModemPipe pipe;
};

View File

@ -671,7 +671,7 @@ const char *maple_device_types[] =
"Keyboard",
"Mouse",
"Twin Stick",
"Ascii Stick",
"Arcade/Ascii Stick",
"Maracas Controller",
"Fishing Controller",
"Pop'n Music controller",
@ -2349,39 +2349,61 @@ static void gui_display_settings()
ImGui::PopStyleVar();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Advanced"))
if (ImGui::BeginTabItem("Network"))
{
ImGuiStyle& style = ImGui::GetStyle();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding);
header("CPU Mode");
{
ImGui::Columns(2, "cpu_modes", false);
OptionRadioButton("Dynarec", config::DynarecEnabled, true,
"Use the dynamic recompiler. Recommended in most cases");
header("Network Type");
{
DisabledScope scope(game_started);
int netType = 0;
if (config::GGPOEnable)
netType = 1;
else if (config::NetworkEnable)
netType = 2;
else if (config::BattleCableEnable)
netType = 3;
ImGui::Columns(4, "networkType", false);
ImGui::RadioButton("Disabled", &netType, 0);
ImGui::NextColumn();
OptionRadioButton("Interpreter", config::DynarecEnabled, false,
"Use the interpreter. Very slow but may help in case of a dynarec problem");
ImGui::Columns(1, NULL, false);
ImGui::RadioButton("GGPO", &netType, 1);
ImGui::SameLine(0, style.ItemInnerSpacing.x);
ShowHelpMarker("Enable networking using GGPO");
ImGui::NextColumn();
ImGui::RadioButton("Naomi", &netType, 2);
ImGui::SameLine(0, style.ItemInnerSpacing.x);
ShowHelpMarker("Enable networking for supported Naomi and Atomiswave games");
ImGui::NextColumn();
ImGui::RadioButton("Battle Cable", &netType, 3);
ImGui::SameLine(0, style.ItemInnerSpacing.x);
ShowHelpMarker("Emulate the Taisen (Battle) null modem cable for games that support it");
ImGui::Columns(1, nullptr, false);
OptionSlider("SH4 Clock", config::Sh4Clock, 100, 300,
"Over/Underclock the main SH4 CPU. Default is 200 MHz. Other values may crash, freeze or trigger unexpected nuclear reactions.",
"%d MHz");
}
ImGui::Spacing();
header("Network");
{
config::GGPOEnable = false;
config::NetworkEnable = false;
config::BattleCableEnable = false;
switch (netType) {
case 1:
config::GGPOEnable = true;
break;
case 2:
config::NetworkEnable = true;
break;
case 3:
config::BattleCableEnable = true;
break;
}
}
if (config::GGPOEnable || config::NetworkEnable || config::BattleCableEnable) {
ImGui::Spacing();
header("Configuration");
}
{
if (config::GGPOEnable)
{
DisabledScope scope(game_started);
OptionCheckbox("Broadband Adapter Emulation", config::EmulateBBA,
"Emulate the Ethernet Broadband Adapter (BBA) instead of the Modem");
}
OptionCheckbox("Enable GGPO Networking", config::GGPOEnable,
"Enable networking using GGPO");
OptionCheckbox("Enable Naomi Networking", config::NetworkEnable,
"Enable networking for supported Naomi games");
if (config::GGPOEnable)
{
config::NetworkEnable = false;
config::NetworkEnable = false;
OptionCheckbox("Play as Player 1", config::ActAsServer,
"Deselect to play as player 2");
char server_name[256];
@ -2415,10 +2437,10 @@ static void gui_display_settings()
}
}
OptionCheckbox("Network Statistics", config::NetworkStats,
"Display network statistics on screen");
}
else if (config::NetworkEnable)
{
"Display network statistics on screen");
}
else if (config::NetworkEnable)
{
OptionCheckbox("Act as Server", config::ActAsServer,
"Create a local server for Naomi network games");
if (!config::ActAsServer)
@ -2436,17 +2458,65 @@ static void gui_display_settings()
ImGui::SameLine();
ShowHelpMarker("The local UDP port to use");
config::LocalPort.set(atoi(localPort));
}
}
else if (config::BattleCableEnable)
{
char server_name[256];
strcpy(server_name, config::NetworkServer.get().c_str());
ImGui::InputText("Peer", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr);
ImGui::SameLine();
ShowHelpMarker("The peer to connect to. Leave blank to find a player automatically on the default port");
config::NetworkServer.set(server_name);
char localPort[256];
sprintf(localPort, "%d", (int)config::LocalPort);
ImGui::InputText("Local Port", localPort, sizeof(localPort), ImGuiInputTextFlags_CharsDecimal, nullptr, nullptr);
ImGui::SameLine();
ShowHelpMarker("The local UDP port to use");
config::LocalPort.set(atoi(localPort));
}
}
ImGui::Spacing();
header("Network Options");
{
OptionCheckbox("Enable UPnP", config::EnableUPnP, "Automatically configure your network router for netplay");
OptionCheckbox("Broadcast Digital Outputs", config::NetworkOutput, "Broadcast digital outputs and force-feedback state on TCP port 8000. "
"Compatible with the \"-output network\" MAME option. Arcade games only.");
{
DisabledScope scope(game_started);
OptionCheckbox("Broadband Adapter Emulation", config::EmulateBBA,
"Emulate the Ethernet Broadband Adapter (BBA) instead of the Modem");
}
}
#ifdef NAOMI_MULTIBOARD
ImGui::Text("Multiboard Screens:");
ImGui::Spacing();
header("Multiboard Screens");
{
//OptionRadioButton<int>("Disabled", config::MultiboardSlaves, 0, "Multiboard disabled (when optional)");
OptionRadioButton<int>("1 (Twin)", config::MultiboardSlaves, 1, "One screen configuration (F355 Twin)");
ImGui::SameLine();
OptionRadioButton<int>("3 (Deluxe)", config::MultiboardSlaves, 2, "Three screens configuration");
}
#endif
ImGui::PopStyleVar();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Advanced"))
{
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding);
header("CPU Mode");
{
ImGui::Columns(2, "cpu_modes", false);
OptionRadioButton("Dynarec", config::DynarecEnabled, true,
"Use the dynamic recompiler. Recommended in most cases");
ImGui::NextColumn();
OptionRadioButton("Interpreter", config::DynarecEnabled, false,
"Use the interpreter. Very slow but may help in case of a dynarec problem");
ImGui::Columns(1, NULL, false);
OptionSlider("SH4 Clock", config::Sh4Clock, 100, 300,
"Over/Underclock the main SH4 CPU. Default is 200 MHz. Other values may crash, freeze or trigger unexpected nuclear reactions.",
"%d MHz");
}
ImGui::Spacing();
header("Other");

View File

@ -68,7 +68,8 @@ public:
V40,
V41,
V42,
Current = V42,
V43,
Current = V43,
Next = Current + 1,
};

View File

@ -121,6 +121,7 @@ Option<bool> NetworkStats("", false);
Option<int> GGPOAnalogAxes("", 0);
Option<bool> NetworkOutput(CORE_OPTION_NAME "_network_output", false);
Option<int> MultiboardSlaves("", 0);
Option<bool> BattleCableEnable("", false);
// Maple

View File

@ -32,7 +32,7 @@ TEST_F(SerializeTest, SizeTest)
std::vector<char> data(30000000);
Serializer ser(data.data(), data.size());
dc_serialize(ser);
ASSERT_EQ(28191382u, ser.size());
ASSERT_EQ(28191417u, ser.size());
}