Closed jonbarrow closed 5 months ago
Forgot to include this in the original breakdown, adding here. This is the decomp of ComputeRetransmitTimeout
(cleaned and modified for clarity):
uint nn::nex::PRUDPEndPoint::ComputeRetransmitTimeout(nn::nex::PRUDPEndPoint *this, nn::nex::PacketOut *packet) {
nn::nex::StreamSettings *stream_settings;
int rtt_avg;
int rtt_dev;
uint uVar1;
uint uVar2;
code *pcVar3;
double dVar4;
double extraout_f1;
double extraout_f1_00;
double dVar5;
stream_settings = (nn::nex::StreamSettings *)GetValue__Q3_2nn3nex55PseudoGlobalVariable__tm__27_Q3_2nn3nex14StreamSettingsFv_RZ1Z(&DAT_104bd2a8 + this->prudp_stream->stream_type * 0x200);
if (DAT_101ac494 != (code *)0x0) {
pcVar3 = DAT_101ac494;
rtt_avg = nn::nex::RTT::GetRTTSmoothedAvg(&this->field_0xa4);
rtt_dev = nn::nex::RTT::GetRTTSmoothedDev(&this->field_0xa4);
uVar1 = (*pcVar3)(rtt_avg + rtt_dev * 4,packet->resend_count);
return uVar1;
}
if ((packet->type_flags & 0xf) == 0) {
dVar5 = extraout_f1;
rtt_avg = nn::nex::StreamSettings::GetSynInitialRTT(stream_settings);
dVar4 = (double)(float)((double)CONCAT44(0x43300000,rtt_avg * (uint)packet->resend_count) - 4503599627370496.0);
nn::nex::PRUDPEndPoint::GetTimeoutMultiplier(this, packet->resend_count, stream_settings);
dVar4 = dVar4 * dVar5;
} else {
rtt_avg = nn::nex::RTT::GetRTTSmoothedAvg(&this->field_0xa4);
rtt_dev = nn::nex::RTT::GetRTTSmoothedDev(&this->field_0xa4);
dVar4 = (double)(float)((double)CONCAT44(0x43300000, (rtt_avg + rtt_dev * 4) * (uint)packet->resend_count) - 4503599627370496.0);
dVar5 = extraout_f1_00;
nn::nex::PRUDPEndPoint::GetTimeoutMultiplier(this, packet->resend_count, stream_settings);
dVar4 = dVar4 * dVar5;
}
if (2.147484e+09 <= (float)dVar4) {
uVar1 = (int)((float)dVar4 - 2.147484e+09) + 0x80000000;
uVar2 = stream_settings->field151_0xb8;
if (uVar1 <= uVar2) {
return uVar1;
}
} else {
uVar2 = stream_settings->field151_0xb8;
if ((uint)(int)dVar4 <= uVar2) {
return (int)dVar4;
}
}
return uVar2;
}
It bases the timeout time based on the connections RTT, packet type, and a multiplier. The multiplier is determined like so:
void nn::nex::PRUDPEndPoint::GetTimeoutMultiplier(nn::nex::PRUDPEndPoint *this, ushort resend_count, nn::nex::StreamSettings *stream_settings) {
uint uVar1;
undefined2 in_register_00000010;
uint uVar2;
uVar2 = CONCAT22(in_register_00000010, resend_count);
uVar1 = nn::nex::StreamSettings::GetExtraRetransmitTimeoutTrigger(stream_settings);
if (uVar2 < uVar1) {
nn::nex::StreamSettings::GetRetransmitTimeoutMultiplier(stream_settings);
return;
}
nn::nex::StreamSettings::GetExtraRetransmitTimeoutMultiplier(stream_settings);
return;
}
Obviously this decomp isn't perfect, since GetTimeoutMultiplier
is returning void
right now. But it's a good start.
The CONCATXY
functions are specific to Ghidra. CONCATXY
takes in 2 values and concats their bytes, where X
and Y
are the sizes in bytes of the 2 numbers. So for example a call like CONCAT43(0x43300000, 0xFFFFF)
would do (0x43300000 << 4*8) | 0xFFFFF
and produce 0x43300000000FFFFF
.
I've found what DAT_101ac494
is in ComputeRetransmitTimeout
. It's an optional function for calculating the timeout. This is likely provided as a way for developers to define their own handling here. It's definied in nn::nex::PRUDPEndPoint::SetCalcRetransmissionTimeoutCallback
, and takes in 2 values:
SRTT + (4*RTTVAR)
)It then returns the value from this function as the timeout.
Since this is likely something set per-game, as an option, we likely don't need to implement it ourselves. Just the same interface for setting the custom handler.
I have just checked the implementation in WATCH_DOGS and SendReliable
is handled slightly differently here, though some of it makes sense. In WATCH_DOGS only a single SlidingWindow
is checked before sending the packet, though this would make sense if it uses packets pre-PRUDPv1.
The weird part about the check is that it checks for the client's connection state to be anything besides 6? We only know of states 0-4, I have no idea what this state is and Xenoblade doesn't make it.
WATCH_DOGS shows that the uVar1
(new_resend_count
) checks in Xenoblade seem to be just debug statistics, which we can likely ignore.
There also is no implementation of ComputeRetransmitTimeout
here, WATCH_DOGS instead in-lines the calculations entirely, though the calculations are mostly the same it seems?
There is an additional check for a field on the PRUDPEndPoint
, however I believe we can safely ignore this as well. All it does is set the multiplier to 1 if the value is false
, however it's always set to true
and no code updates it. It's possible this is also just a debug feature.
Decomp (modified and cleaned for clarity):
void rdv::PRUDPEndPoint::SendReliable(rdv::PRUDPEndPoint *this, rdv::PacketOut *packet) {
bool more_pending;
rdv::StreamSettings *stream_settings;
rdv::Timeout *packet_timeout;
uint extra_retransmit_timeout_trigger;
uint stream_id;
uint packet_size;
ushort new_resend_count;
uint resend_count;
int retransmit_time_base;
uint retransmit_time_base_multiplier;
double retransmit_multiplier;
int rto;
// If packet is DISCONNECT and client is not in state 6 and has more packets to send
if (
(
((packet->flags_and_type & 7) == 3) && (this->connection_state != 6)
) && (more_pending = rdv::SlidingWindow::DataPending(this->sliding_window), more_pending)
) {
rdv::TimeoutManager::SchedulePacketTimeout(&this->prudp_stream->timeout_manager, packet);
return;
}
// Increase the packets resend count
new_resend_count = packet->resend_count + 1;
resend_count = (uint)new_resend_count;
packet->resend_count = new_resend_count;
retransmit_time_base_multiplier = resend_count;
stream_settings = rdv::Stream::GetSettings((rdv::Stream *)this->prudp_stream);
// Likely some debug setting? This is always
// set to true in WATCH_DOGS, and is never
// updated. Haven't found a use for this
if (stream_settings->field39_0x4e == false) {
retransmit_time_base_multiplier = 1;
}
// this->rtt is updated by rdv::RTT::Adjust(this->rtt, new_value)
// inside both rdv::PRUDPEndPoint::PacketAcknowledged()
// and rdv::PRUDPEndPoint::ServiceIncomingPacket()
// this->rtt refers to the first field of the rdv::RTT which is
// the estimated RTT
//
// I have no idea what this->field103_0x80 is. It seems to never be
// referenced outside of this function, even in the rdv::PRUDPEndPoint
// constructor function
retransmit_time_base = (this->rtt >> 3) + (this->field103_0x80 & 0xFFFFFFFCU);
extra_retransmit_timeout_trigger = rdv::StreamSettings::GetExtraRetransmitTimeoutTrigger(stream_settings);
// Calculate the multiplier
if (resend_count < extra_retransmit_timeout_trigger) {
retransmit_multiplier = (double)rdv::StreamSettings::GetRetransmitTimeoutMultiplier(stream_settings);
retransmit_multiplier = (double)(float)((double)CONCAT44(0x43300000, retransmit_time_base * retransmit_time_base_multiplier) -4503599627370496.0) * retransmit_multiplier;
} else {
retransmit_multiplier = (double)rdv::StreamSettings::GetExtraRetransmitTimeoutMultiplier(stream_settings);
retransmit_multiplier = (double)(float)((double)CONCAT44(0x43300000, retransmit_time_base * retransmit_time_base_multiplier) -4503599627370496.0) * retransmit_multiplier;
}
// Prevent the multiplier from overflowing
// the uint32 limit
if (2.147484e+09 <= (float)retransmit_multiplier) {
rto = (int)((float)retransmit_multiplier - 2.147484e+09) + 0x80000000;
} else {
rto = (int)retransmit_multiplier;
}
// Packet has been resent more than once
if (1 < resend_count) {
rdv::TransportPerfCounters::Inc(this->prudp_stream->field12_0xc + 0x10, 7, 1);
rdv::TransportPerfCounters::Inc(&this->field161_0xc8, 7, 1);
}
// Update the RTO, start the timeout, and send the packet
packet_timeout = rdv::PacketOut::GetTimeout(packet);
rdv::Timeout::SetRTO(packet_timeout, rto);
rdv::TimeoutManager::SchedulePacketTimeout(&this->prudp_stream->timeout_manager, packet);
stream_id = rdv::StationURL::GetStreamID(&this->station_url);
rdv::PRUDPStream::Send(this->prudp_stream, this->field74_0x50, (uchar)stream_id, packet);
// If packet is DATA. Likely some debug stats? Haven't found a use for this
if ((packet->flags_and_type & 7) == 2) {
packet_size = rdv::Packet::GetSize(packet);
rdv::TransportPerfCounters::Inc(&this->field161_0xc8, 0, packet_size);
}
return;
}
Given that they have different methods of calculating the RTO, I'd say it's safe to assume it doesn't matter THAT badly how we do it? We should just pick an implementation and stick with it. I think the WATCH_DOGS implementation is the simpler one, but if we want to be closer to NEX then we should use the Xenoblade implementation. Regardless of implementation though I believe we should still use the SetCalcRetransmissionTimeoutCallback
method so games can define this entirely themselves if they wish.
rdv::RTT::Adjust()
looks like (modified and cleaned for clarity):
void rdv::RTT::Adjust(rdv::RTT *this, int new_rtt) {
int difference;
// Calculate the difference between the new RTT measurement
// and 1/8 of the current estimated RTT
difference = new_rtt - ((uint)this->estimated >> 3);
// Always positive
if (difference < 0) {
difference = -difference;
}
// Update the estimation, last RTT, and variance
this->estimated = this->estimated + difference;
this->last = new_rtt;
this->variance = this->variance + (difference - ((uint)this->variance >> 2));
return;
}
Checked Existing
What enhancement would you like to see?
Implement the
TimeoutManager
andTimeout
classes to replace the current timeout/scheduler mechanism. This directly relates to both https://github.com/PretendoNetwork/nex-go/issues/49 and https://github.com/PretendoNetwork/nex-go/pull/60 (though this should probably be implemented outside of that PR).We currently do not implement timeouts and packet retransmission the way NEX/QRV does at all, and it's half-baked at best. For more accurate emulation we should actually implement the relevant classes and use them.
Depending on how we approach this, this is not a trivial thing to implement, as it would require several other structural changes. But I'm not 100% sure we need those changes right now? Some of this information was already went over here https://github.com/PretendoNetwork/nex-go/pull/58#issuecomment-2132365836, but I'll restate it here as well.
PRUDPEndPoint
has aSendReliable
method, which takes in aPacketOut
DISCONNECT
and if the endpointsmax_substreams
is NOT 0xFFFFFFFFSlidingWindow
is pulledDataPending
is calledSlidingWindow
still has pending packetsfield664_0x2b8
onPRUDPEndPoint
is incremented by 1SchedulePacketTimeout
is called on theTimeoutManager
which is stored on thePRUDPStream
which is stored onPRUDPEndPoint
(this->prudp_stream->timeout_manager
), taking in thePacketOut
SendReliable
returns. This means that if the client still has packets to be sent and tries to disconnect, it will continue to send those packets before sending theDISCONNECT
packetSlidingWindow
has been checkedresend_count
is incremented by 1 (noticed how the first branch does not do this)PRUDPEndPoint
retransmit_timeout
value is calculated usingComputeRetransmitTimeout
onPRUDPEndPoint
SetRTO
(Set Retransmit Time Out?) method on theTimeout
stored on thePacketOut
SchedulePacketTimeout
is called on theTimeoutManager
which is stored on thePRUDPStream
which is stored onPRUDPEndPoint
(this->prudp_stream->timeout_manager
), taking in thePacketOut
(notice how the first branch does not callSetRTO
)StreamSettings
is pulled and a value is checked. If the value is not null thenlast_send_time
onPRUDPEndPoint
is set to the current timeSend
on thePRUDPStream
taking inPRUDPEndPoint
port numberPacketOut
PacketEncDec
onPRUDPEndPoint
Decomp (cleaned and modified for clarity):
SchedulePacketTimeout
follows the following logic:nn::nex::Network::GetLock
is called to get a lockSYN
,CONNECT
orDISCONNECT
, then set the timeout to immediately expire? Unsure what the purpose of this is, but NEX has this disabledDecomp (cleaned and modified for clarity):
The actual timeout handling is handled by 3 different methods:
nn::nex::PRUDPStream::DoWork()
. Presumably this is called on some interval, every frame? I can't find a direct reference to this. It checks if it should kill itself and does cleanup if so. Otherwise it does worknn::nex::TimeoutManager::ServicePacketTimeouts()
. This loops overtimes
on the timeout manager and checks for any expired timeouts throughnn::nex::Timeout::IsExpired()
on each packet and does some cleanup? The loopbreak
s if a timeoutIsAwaited
andIsExpired
are both false, and never reaches the following code?nn::nex::TimeoutManager::ServicePacketTimeouts(nn::nex::PacketOut*)
is called if timed out. This is where the ACTUAL timeout handling seems to happen. If the packetsresend_count
is less than themax_resends
value fromStreamSettings
, ANDnn::nex::Timeout::IsExpired()
on the packets timeout is 0 (false?) thenSendReliable
is called again. This is where the loop beginsThis can easily be reimplemented using contexts, like shown in https://github.com/PretendoNetwork/nex-go/issues/49. However we have to decide on how to structure this, and where to store some of these things. We do not currently have a
PRUDPStream
implementation, which means we can't store theTimeoutManager
there?This also means we should probably bring back
SlidingWindow
for sending packets.Any other details to share? (OPTIONAL)
All information is coming from Xenoblade on the Wii U, though it should be applicable to all other NEX/RDV titles. I just find the implementations in Xenoblade to be more readable