Calls that checked one byte of a game's buffer and then wrote or read the
whole thing, or trusted a negative length:
- sceNetEtherNtostr/Strton, the apctl BSS descriptor calls, GetState and
sceNetGetDropRate.
- sceNpAuthGetTicket wrote its whole ticket whatever the length; it's
now built locally and written once the buffer is known to hold it.
sceNpAuthGetTicketParam checked its limits after copying, and walked
ticket params past the ticket's end.
- sceNp2's communication ID and passphrase, a negative maxServerIds, and
an unchecked unknown1Ptr.
- sceParseUri's work area and scheme string, sceParseHttp's negative
header length and value address, and ReadVariableSizedStruct's size.
- sceKernelCreateLwMutex initialized the work area without checking it
at all, and the other LwMutex calls checked one byte of it.
- sceNetAdhocctlGetScanInfo cleared the length the game gave, of a
buffer checked by one byte (or any length, negative ones included).
Also: a game mode join inserted at begin() + 1 into a possibly empty
list of players.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its entry in sendTargetPeers stayed, so the next blocking connect from
the same thread on a socket that got the same id returned BUSY
(pspautotests net/adhoc/pair/basic, connect to a closed port, after a
connected socket was closed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PSP keeps what arrives in the socket's buffer, of the size given to
PdpCreate, and drops a datagram that doesn't fit: a buffer of 0 gets
nothing, one of 2048 bytes only some of ten 1000-byte datagrams
(pspautotests net/adhoc/pair/flow). Each transport now only hands over
the datagrams that have arrived (PdpTakeDatagram), into a queue per
socket that is filled every millisecond and whenever the game looks, and
PdpRecv, its blocking retry, GetPdpStat, PollSocket and game mode read
from that. This replaces each transport's own receive paths, so the
relay now reports the sender with NOT_ENOUGH_SPACE too, and P2P's
blocking receive no longer misreads a stale errno.
A datagram to ourselves goes straight into our own socket's queue, as on
a PSP, so it's there at once through the relay too (net/adhoc/term).
Also from comparing the transports:
- P2P: a blocking Accept of a connection from outside the group keeps
waiting instead of returning -1.
- Relay: an accepted socket gets an id of its own, not its listener's,
which blocking calls are told apart by.
- ALERT in PollSocket comes from the socket in both transports.
- No send/receive timeouts are set on the game's sockets: all host
sockets are non-blocking, and in relay mode the ids aren't host
sockets at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A connect to a port nobody listens on is refused within a second on a
PSP, and every later connect on that socket is refused too, even once
something listens there (pspautotests net/adhoc/pair/basic,
pair/ptpclose). PPSSPP recreated the socket and tried again until the
game's timeout ran out, to cover a peer that wasn't listening yet.
Dissidia 012 and Fate/Unlimited Codes, named in the old comments, still
get to their matches.
Also set the established time wherever a connection becomes established,
which the timing of a vanished peer's failure counts from.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
How long an adhocctl request waits for the server before giving up, and
how long a joiner's GAME event waits for the host to show up, were
measured in wall time, so a game saw them shorter when sped up and
longer when slowed down.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PSP completes a connect to a listener without the game calling Accept,
so an opened PTP connection is up a few ms after PtpOpen (pspautotests
net/adhoc/pair/ptppoll). The relay only answers the connecting side once
the listening side accepts, so the relay transport now accepts pending
connections into a queue as long as the listener's backlog, from a new
per-millisecond AdhocTransport::Poll() and from polls and accepts, and
PtpAccept takes them from there. A poll reports ACCEPT while the queue
isn't empty.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each started context had two host threads, polling every 10 ms of wall
time: one sent queued packets, received, and ran hellos, keepalives and
timeouts, and the other handed events to the callbacks. A single
CoreTiming event now does both for every running context, every 1 ms of
emulated time, and Stop does what the threads did on their way out.
Matching no longer depends on host scheduling or wall-clock delays, so an
accept's ESTABLISHED comes 1-10 ms after SelectTarget at any emulation
speed, as on a PSP (pspautotests net/adhoc/pair/matchingaccept).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RelayTransport, with the aemu_postoffice helpers only it uses, goes to
AdhocTransportRelay.cpp, and P2PTransport to AdhocTransportP2P.cpp. The
helpers they share with sceNetAdhoc.cpp are declared in
AdhocTransport.h. Moved as they were.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The relay and P2P halves of PtpOpen, PtpListen, PtpClose, PtpSend,
PtpRecv, PtpAccept and their blocking retries, GetPtpStat, PtpFlush,
PollSocket and deleting all sockets move into RelayTransport and
P2PTransport. Connecting stays shared, with the transports answering
the steps that differ (start, check on, confirm, still pending).
No change in behaviour, except that a relay poll asks the postoffice
whether a socket is readable once instead of twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The relay and P2P halves of PdpCreate, PdpDelete, PdpSend, PdpRecv, the
blocking PDP receive, GetPdpStat and game mode's readiness checks move into
RelayTransport and P2PTransport, which Transport() picks between by
serverHasRelay. No change in behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As on a PSP (pspautotests net/adhoc/abandon): the Disconnect fails and
the join goes on to CONNECT. Before, it was accepted, and the join's late
answer from the server could still connect us afterwards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the game gives up on a join (game mode timed out, or it
disconnected), a join still queued isn't sent anymore, and the rest of
the server's answer to one already sent is dropped: its players, and the
BSSID that came last and connected us. Players joining a group we've
just left (before the server knew) are dropped too, instead of staying
in our peer list.
Replaces the previous fix, which only dropped a late BSSID: on Windows
the join itself was still queued when game mode timed out in
net/adhoc/gamemode, and went out after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When entering game mode timed out before the server's group BSSID came
back, the late BSSID was taken for a normal group's, and the state went
to CONNECTED: every later CreateEnterGameMode then got ALREADY_CONNECTED.
At the ad hoc tests' 8x speed the 1 s timeouts in net/adhoc/gamemode are
125 ms of wall time, short enough for that on Windows; at 50x it happens
on macOS too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The timeouts of blocking socket calls and of entering game mode are the
PSP's, but they were measured on the wall clock, so they came out
shorter in PSP time whenever emulation ran fast or slow. The wait for
the ad hoc server, which is a host-side round trip, stays on the wall
clock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS, calling connect() again on a socket whose non-blocking
connect was refused returns EISCONN; only SO_ERROR and a failing
getpeername tell it apart from a connection. PtpConnect took EISCONN at
its word, so in P2P mode a connect to a port nobody listened on
"succeeded", and the game sent into nothing. Check getpeername first
and use SO_ERROR when it fails.
PollSocket also reports a refused connect as DISCONNECT now, as a PSP
does (net/adhoc/poll), instead of only ever noticing success.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PdpCreate, PtpOpen and PtpListen use the local MAC whatever the game
passes (a stale MAC from an old savestate would otherwise break them),
but they did it by overwriting the game's copy. A game that passes
someone else's MAC from a table then has its own MAC in that table.
pspautotests net/adhoc/pdp passes its "other" MAC to PdpCreate and later
sends to it; every one of those sends came back to itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PSP stays in its group through sceNetAdhocTerm and sceNetAdhocInit,
and matching keeps working; only the sockets go, and calls on them
return NOT_INITIALIZED until the next init (net/adhoc/term, recorded on
a PSP). We also terminated adhocctl, so a game that restarts the adhoc
library inside a group found itself disconnected. Every other caller
already terminates adhocctl itself first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PSP hands out socket IDs from 1 again after sceNetAdhocTerm and
sceNetAdhocInit (net/adhoc/socketids, recorded on a PSP). We kept
counting up. Pangya: Fantasy Golf tears down the network between its
room and the round, and then keeps using socket 1 without looking at
what PdpCreate returned, so every send and receive failed with
INVALID_SOCKET_ID and each player saw the other as "Off-line".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PSP sends its master every 12 ms after the initial sync, and a peer's
replica reports each one as an update. PPSSPP only sent a master that
had changed, so a replica polled until updated waited for the next
change. Also take every packet waiting each period, not one: every
member sends each period. Measured in pspautotests
net/adhoc/pair/gamemodesync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the peer has left, a PSP drops the connection once data sent to it
has gone unacknowledged for (rexmt_cnt + 1) * rexmt_int. A connection
that only receives never fails. Count from the first data the peer
couldn't acknowledge: sent after it left, or within 10 ms before
(pspautotests net/adhoc/pair/vanishtiming).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetPtpStat only looked at a connection in progress 33 ms after the last
connect attempt, and PollSocket reported CONNECT 1 ms after it whether
the connection was up or not. On a PSP both show it the moment it's up,
about 5 ms after the connect (pspautotests net/adhoc/pair/ptpconnect).
The old timings were tuned for GvG Next Plus, Bleach and Fate; Fate's
network match still works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GAMEMODE comes 4.38 s after CreateEnterGameMode, or 1.35 s after
JoinEnterGameMode or the last member's arrival, whichever is later,
instead of a fixed 2 s after the last member (plus 60 ms for a member,
a workaround for Pocket Pool). DISCONNECT comes 650 ms after
ExitGameMode. Measured in pspautotests net/adhoc/pair/gamemodetiming.
A member that joins early still gets GAMEMODE before the host: it
doesn't know when the host entered game mode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pspautotests net/adhoc/pair/gamemode:
- CreateMaster with size 0 fails with INVALID_DATALEN.
- CreateReplica for a MAC that isn't in the game mode group fails with
0x80410D13. PPSSPP made one, and then blocked the thread forever
waiting to sync with it.
- CreateReplica for the same MAC again gives ALREADY_CREATED. The check
compared the pointer instead of the MAC, so it never matched.
- A replica's ID is the member's place in the member list, host first:
the client's replica of the host is 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where pspautotests net/adhoc/timing or net/adhoc/pair/timing measured
a value, use it:
- Call costs: PdpSend 21 us (10 broadcast), PtpSend 122, PtpFlush 196,
GetPdpStat 1, GetPtpStat 2, PollSocket 18, GetScanInfo 2, GetPeerList
and MatchingGetMembers 4 (no longer a 100 us reschedule),
MatchingStart 416 instead of 30 ms, sceNetAdhocctlTerm 154,
sceNetAdhocTerm 10 and sceNetTerm 209 instead of 10 ms each,
GameModeUpdateMaster 80, UpdateReplica 9.
- Game mode data goes out every 12 ms, not every 0.5 ms.
- DISCONNECT comes 640 ms after ExitGameMode, not 105 ms.
- CONNECT after a Join no longer waits for a peer: a PSP delivers it
before the host shows up in the peer list.
Several of the old values were tuned for particular games (BattleZone's
frame rate, Fate Unlimited Codes and Bleach through PollSocket), and
may have been making up for other differences that are fixed now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PSP checks a PTP connection's oldest unacknowledged data every
rexmt_int, counting from when the connection was established, and drops
the connection at the first check where that data is older than
(rexmt_cnt + 1) * rexmt_int. A matching peer times out keepalive_int *
init_count after its last keepalive, checked all the time instead of on
the console's own keepalive tick, which could add up to an interval.
Both measured with several settings in pspautotests
net/adhoc/pair/vanishtiming; DATA_TIMEOUT already matched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the basic and lifecycle pair tests, recorded on two PSPs:
- GetNameByAddr for the console's own MAC gives NO_ENTRY, and
GetAddrByName doesn't find the console itself.
- GetPeerInfo with a size too small for the entry gives INVALID_BUFLEN.
- A PTP connection accepted over the relay reports the peer's port as
the game sees it, not the relay's offset port.
- The built-in server makes up a locally administered BSSID for each
group, which stays when the creator leaves. It was the MAC of the
oldest member.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vanish pair test (recorded on two PSPs) now passes on both sides,
over the relay:
- From the Disconnect call, a console's PDP and PTP sends succeed and go
nowhere, and nothing reaches it.
- A PTP connection to a peer that has left the group fails with
DISCONNECTED after rexmt_int * rexmt_cnt. Relay sockets now get their
retry settings (and are zeroed), and an accepted one counts as
connected.
- Matching data to a peer that has left gets DATA_TIMEOUT after the
retransmissions instead of a DATA_ACK.
- The matching timeout for a silent peer is keepalive_int * init_count,
plus MATCHING_TIMEOUT_EXTRA_US (0) to tune for internet latency. It
was (keepalive + rexmt) * count + 500ms, later than a PSP's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a PSP (pspautotests net/adhoc/gamemode):
- a JoinEnterGameMode whose host never shows up gets ERROR with TIMEOUT
when its timeout runs out, and is out of the group afterwards. The
joiner's GAMEMODE, queued as soon as the server answered and held
until the host appeared, came after 5 s anyway;
- a CreateEnterGameMode whose first member isn't this console returns
0 and reports INVALID_ADDR;
- in a normal group, CreateEnterGameMode reports ALREADY_CONNECTED and
stays in the group, and ExitGameMode returns INVALID_IBSS;
- ExitGameMode outside game mode returns 0 with no event.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a PSP they return within about 30 µs (pspautotests net/adhoc/timing).
They waited 10 ms before trying to send the request; now the first try is
right away, and the call returns once it's sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PSP delivers CONNECT 1.1 s after a Create, 0.53 s after a Join and
3.5-4 s after a Connect (which looks for the group first), SCAN 1.0 s
after a Scan and DISCONNECT 105 ms after a Disconnect, and the state
changes along with the event (pspautotests net/adhoc/timing and
net/adhoc/pair/timing). Deliver them then, counting from the request in
emulated time, or later if the server's answer takes longer. This
replaces the delays tuned for Dissidia 012, GTA: VCS and Tekken 5, and
Disconnect no longer blocks for 16.7 ms (Tekken 6): it returns at once,
as on a PSP.
Since the state now changes later, a Disconnect can come while a Create
is still pending. Keep track of whether the server has us in a group, so
the Disconnect still reaches it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a PSP, Init returns at once and a Create, Connect or Scan right after
it works (pspautotests net/adhoc/ctlinit). The login now runs on in the
background, and those requests wait for it, as they already did when it
was lost. The first login no longer resets the busy flag, which would
have cleared it for a request already waiting on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The friendFinder thread is replaced by friendFinderPoll(), which a
CoreTiming event calls every millisecond of emulated time on the CPU
thread. It handles everything the server has sent since the last poll,
instead of one event every 10 ms, and pings it at the same real-time
interval. Resolving the server and logging in, which can take seconds,
run on a thread of their own; the poll starts it and takes over the
session when it's done.
Everything else about the client is unchanged, including sceNetAdhocctlInit
waiting for the login. The savestate section goes to version 11 for the
poll event; older states get it scheduled on load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The console itself isn't in its peer list, and a PSP returns NO_ENTRY
for it (pspautotests net/adhoc/adhocctl).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With nobody to find (pspautotests net/adhoc/discover): InitStart leaves
the result field alone and fails with 0x80411301 while running,
adhocctl enters the DISCOVER state after the first Update, and after a
Stop adhocctl goes back to DISCONNECTED with a DISCONNECT event, then
the status becomes COMPLETED with the result cancelled. RequestSuspend
while discovering fails with 0x80411303 instead of cancelling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a buffer smaller than the next datagram, a PSP returns
NOT_ENOUGH_SPACE with the datagram's size, copies nothing, and keeps
the datagram for the next call (pspautotests net/adhoc/pdp). The first
bytes were copied into the buffer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a Recv has found the peer gone, a PSP keeps returning
DISCONNECTED for Send and Recv on that socket, not NOT_CONNECTED
(pspautotests net/adhoc/ptp).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PSP numbers adhoc sockets in increasing order instead of reusing the
lowest free ID, and a PDP or PTP create that fails its address or port
check, or finds the PDP port in use, uses up an ID too (pspautotests
net/adhoc/pdp and net/adhoc/ptp). Find free slots starting after the last
one handed out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a PSP (pspautotests net/adhoc/poll):
- a PTP listener reports SEND and FLUSH, and ACCEPT with a connection
waiting;
- a connected PTP socket reports SEND, CONNECT and FLUSH, and RECV with
data waiting;
- a connection whose peer has closed reports DISCONNECT alone, and the
call returns EXCEPTION_EVENT;
- an unknown ID puts INVALID in the first entry;
- a blocking poll with nothing to report by its timeout returns TIMEOUT,
and one that hits a disconnected socket or the poll alert returns at
once.
A connection PtpOpen started counts as connected once it's up. Sockets
still connecting keep PPSSPP's own events (Fate Unlimited Codes,
Bleach), and a refused connect still isn't reported, since PPSSPP keeps
retrying it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a PSP (pspautotests net/adhoc/alerts), PtpConnect on a connected
socket and PtpFlush with nothing left to send return 0 with their alerts
set, and the poll alert reports ALERT alone in revents. A connection
PtpOpen started counts as connected once it's up, even if no call has
looked at it since.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a PSP, a PDP socket created on port 0 gets a free port between
0x1000 and 0x7FFF (pspautotests net/adhoc/limits). The host picked
one instead, usually 0x8000 or above once the port offset was taken
off. Pick it the way PtpOpen already does for PTP, and try another if
the host port is taken. Port 0 still doesn't become the fixed port the
offset alone would give (Phantasy Star Portable 2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every message through the relay waited for the next pump, every 8 ms,
or 100 ms while nobody was connected, which is when a game's first
connection comes in. That was most of what a round trip took once the
emulators stopped running in frame bursts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Join the old relay thread before starting a new one (assigning to a
joinable std::thread terminates), don't map the UPnP port when another
relay already has it, sleep the intended interval (the elapsed time was
subtracted the wrong way round), use one worker per pool, and guard the
log helper against empty messages. Drop the warning that the built-in
server doesn't support relay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>