PDP and PTP sockets

sceNetAdhoc's two socket types carry almost all game traffic:

  • PDP (Peer Datagram Protocol): unreliable, unordered datagrams, like UDP. Can broadcast.
  • PTP (Peer Transport Protocol): reliable, ordered byte streams between two consoles, like TCP.

Both are addressed by MAC address and a 16-bit port. Socket IDs are small positive integers shared between the two types.

Common rules

Blocking, timeouts and flags

Every call that can wait takes a timeout in microseconds and a flag:

  • flag & 1 (ADHOC_F_NONBLOCK): don't wait. If the call can't complete at once it returns WOULD_BLOCK (0x80410709).
  • Otherwise the call blocks for up to timeout µs, then fails with TIMEOUT (0x80410715). A timeout of 0 means "wait forever".
  • Any other bit set in flag gives INVALID_ARG.

Alerts

sceNetAdhocSetSocketAlert(id, flags) arms a socket so that the next blocking operation of a given kind fails with SOCKET_ALERTED (0x80410708) instead of waiting. This is how one thread interrupts another thread blocked in a receive. sceNetAdhocGetSocketAlert reads the flags back.

FlagAlerts
0x0010send
0x0020receive
0x0040poll
0x0080connect
0x0100accept
0x0200flush

On the PSP (the net/adhoc/poll test):

  • Only these bits are kept; GetSocketAlert reads back flags & 0x3F0. Setting 0 clears them.
  • Alerts persist. Once set, every matching call fails with SOCKET_ALERTED, blocking or not, until the flags are cleared.
  • Setting the receive alert on a socket another thread is blocked receiving on wakes that thread at once with SOCKET_ALERTED. The send alert doesn't wake a receive.
  • Deleting a socket wakes a thread blocked on it with SOCKET_DELETED.

Polling

sceNetAdhocPollSocket(sds, count, timeout, flag) takes an array of SceNetAdhocPollSd { int id; int events; int revents; } (12 bytes each) and returns how many sockets have events, like poll:

BitEvent
0x0001SEND: the socket can send
0x0002RECV: data is waiting
0x0004CONNECT: a PTP connect finished
0x0008ACCEPT: a PTP listener has an incoming connection
0x0010FLUSH: a PTP flush finished
0x0100INVALID: no such socket (reported, not requested)
0x0200DELETE: the socket was deleted (reported, not requested)
0x0400ALERT: an alert fired
0x0800DISCONNECT: the PTP connection was closed (reported, not requested)

On the PSP:

Socketrevents
PDP, idleSEND
PDP, data waitingSEND, RECV
PTP listener, idleSEND, FLUSH
PTP listener, connection waitingSEND, FLUSH, ACCEPT
PTP, connectedSEND, CONNECT, FLUSH (and RECV once data is waiting)
PTP, peer closed or connect refusedDISCONNECT, and the call returns EXCEPTION_EVENT (0x80410717), even with no events requested
Unknown IDINVALID, and the call returns INVALID_SOCKET_ID
Poll alert setALERT, and the call returns EXCEPTION_EVENT

revents only holds the requested events, apart from DISCONNECT, INVALID and ALERT. A count of 0 or less, a NULL array, or a flag other than 0 or 1 gives INVALID_ARG. A blocking poll with nothing to report fails with TIMEOUT; a timeout of 0 waits forever.

Error codes

CodeNameCodeName
0x80410701INVALID_SOCKET_ID0x8041070FSOCKET_ID_NOT_AVAIL
0x80410702INVALID_ADDR0x80410710PORT_NOT_AVAIL
0x80410703INVALID_PORT0x80410711INVALID_ARG
0x80410704INVALID_BUFLEN0x80410712NOT_INITIALIZED
0x80410705INVALID_DATALEN0x80410713ALREADY_INITIALIZED
0x80400706NOT_ENOUGH_SPACE0x80410714BUSY
0x80410707SOCKET_DELETED0x80410715TIMEOUT
0x80410708SOCKET_ALERTED0x80410716NO_ENTRY
0x80410709WOULD_BLOCK0x80410717EXCEPTION_EVENT
0x8041070APORT_IN_USE0x80410718CONNECTION_REFUSED
0x8041070BNOT_CONNECTED0x80410719THREAD_ABORTED
0x8041070CDISCONNECTED0x8041071AALREADY_CREATED
0x8040070DNOT_OPENED0x8041071BNOT_IN_GAMEMODE
0x8040070ENOT_LISTENED0x8041071CNOT_CREATED

The three codes in bold really are 0x8040..., not 0x8041....

PDP

Functions

FunctionDoes
PdpCreate(mac, port, bufsize, flag)Creates a socket bound to the local MAC and port (0 picks a free port). bufsize is the receive buffer. Returns the socket ID.
PdpSend(id, dstmac, dstport, data, len, timeout, flag)Sends one datagram. FF:FF:FF:FF:FF:FF broadcasts to the group.
PdpRecv(id, &srcmac, &srcport, buf, &len, timeout, flag)Receives one datagram. len is the buffer size going in and the datagram size coming out.
PdpDelete(id, flag)Deletes the socket.
GetPdpStat(&size, buf)Lists all PDP sockets. NULL buffer returns the size needed.

SceNetAdhocPdpStat (20 bytes): next (4), id (4), local MAC (6), local port (2), bytes waiting in the receive buffer (4).

Rules on the PSP

Confirmed with the net/adhoc/pdp test in pspautotests:

  • Create:
    • Only works once the console is in a group; before that it gives INVALID_ADDR even for the console's own MAC.
    • The MAC must be the console's own (INVALID_ADDR otherwise, INVALID_ARG for NULL).
    • Port 0 picks a free port; 65535 gives INVALID_PORT; a port in use gives PORT_IN_USE.
    • Any bufsize is accepted, even 0 or negative.
    • The flag must be 0 (INVALID_ARG for anything else, including 1).
  • Send:
    • Returns 0 on success, not the length.
    • A NULL destination or a flag other than 0 or 1 gives INVALID_ARG, before the socket ID is checked.
    • 00:00:00:00:00:00 and multicast addresses other than FF:FF:FF:FF:FF:FF give INVALID_ADDR.
    • Ports 0 and 65535 give INVALID_PORT.
    • Lengths of 0 to 65523 bytes are accepted (0 even with NULL data); longer, negative, or non-zero with NULL data gives INVALID_DATALEN. Anything over 1444 bytes is fragmented on the air (see On the PSP).
    • Sending to a MAC nobody has succeeds.
    • Sending to the console's own MAC is delivered locally; its own broadcasts are not.
  • Send queue full: non-blocking sends return WOULD_BLOCK; blocking ones wait until there's room or the timeout.
  • Recv:
    • Returns 0 on success, with len set to the datagram's size and the sender's MAC and port filled in.
    • NULL MAC and port pointers are allowed; a NULL buffer or length pointer, or a flag other than 0 or 1, gives INVALID_ARG.
    • Nothing waiting: WOULD_BLOCK (non-blocking) or TIMEOUT (blocking), with len left alone.
    • Buffer too small: NOT_ENOUGH_SPACE (0x80400706), with len set to the datagram's size and the sender filled in. Nothing is copied, and the datagram stays queued for the next call.
  • Delete: the flag must be 0; deleting twice gives INVALID_SOCKET_ID.
  • Socket IDs keep increasing rather than reusing freed ones, and creates that fail on the address, port or port-in-use checks use up an ID too.
  • sceNetAdhocTerm returns 0 even when called twice.
  • Delivery is not guaranteed and datagrams can arrive out of order.

Between two consoles (the net/adhoc/pair/basic test, with the consoles 20 cm apart):

  • Datagrams of up to 16000 bytes arrive intact. 32000-byte ones are sent without error but never arrive. Sending 65523 bytes sometimes fails with 0x80410001 (out of memory).
  • Of a burst of 50 small datagrams sent back to back, a few are usually lost; the rest arrive in order.
  • A broadcast reaches the other console. Sending to a port nobody has open succeeds.

PTP

Functions

FunctionDoes
PtpOpen(mac, port, dstmac, dstport, bufsize, rexmt_int, rexmt_cnt, flag)Creates a socket for an outgoing connection and starts connecting. rexmt_int (µs) and rexmt_cnt set the retransmit interval and count. Returns the socket ID.
PtpConnect(id, timeout, flag)Waits for the connection started by Open to be established (or, non-blocking, checks on it).
PtpListen(mac, port, bufsize, rexmt_int, rexmt_cnt, backlog, flag)Creates a listening socket.
PtpAccept(id, &peermac, &peerport, timeout, flag)Waits for an incoming connection and returns a new socket ID for it. The MAC and port pointers may be NULL.
PtpSend(id, data, &len, timeout, flag)Sends; len returns how much was queued.
PtpRecv(id, buf, &len, timeout, flag)Receives whatever is available, up to len.
PtpFlush(id, timeout, flag)Waits until everything sent has been acknowledged.
PtpClose(id, flag)Closes.
GetPtpStat(&size, buf)Lists all PTP sockets.

SceNetAdhocPtpStat (36 bytes): next, id, local MAC, peer MAC, local port, peer port, bytes in the send buffer, bytes in the receive buffer, and the state:

State
0CLOSED
1LISTEN
2SYN_SENT
3SYN_RCVD
4ESTABLISHED

Rules on the PSP

Confirmed with the net/adhoc/ptp test in pspautotests, with the console connecting to itself:

  • Open and Listen:
    • The local MAC must be the console's own (INVALID_ADDR; INVALID_ARG for NULL).
    • bufsize, rexmt_int, rexmt_cnt and backlog aren't checked: 0 is accepted for all of them.
    • Open rejects a NULL destination (INVALID_ARG), a zero destination (INVALID_ADDR) and destination ports 0 and 65535 (INVALID_PORT), but accepts the broadcast address.
    • The flag must be 0 (INVALID_ARG).
  • Connecting:
    • The connection is made in the background by Open. A connection to a listener on the same console is ESTABLISHED by the time Open returns, and PtpConnect then returns 0, also when called again.
    • Until it's accepted, the incoming connection shows up in GetPtpStat with ID -1.
    • A connect to a port nobody is listening on fails at once with CONNECTION_REFUSED, blocking or not.
  • Accept: nothing waiting gives WOULD_BLOCK or TIMEOUT; on a socket that isn't listening, NOT_LISTENED (0x8040070E). The accepted socket reports the connecting socket's MAC and port.
  • Send and Recv:
    • Both return 0 on success. Recv returns whatever is buffered, up to len (100 bytes sent, read as 30 then 70).
    • On a listener they give NOT_CONNECTED.
    • Send with NULL data or a negative length gives INVALID_DATALEN, a NULL length pointer INVALID_ARG; a length of 0 is accepted.
  • When the peer closes, Send and Recv give DISCONNECTED, the socket's state becomes CLOSED, and the closed peer's socket disappears from GetPtpStat.
  • Close: twice gives INVALID_SOCKET_ID; a non-zero flag gives INVALID_ARG.
  • PtpFlush returns 0 once the data is sent.
  • Segments are at most 1444 bytes on the air.

Between two consoles, a 20000-byte transfer arrives intact, a connect to a port nobody listens on fails with CONNECTION_REFUSED within a second, and when one side closes, the other's next receive gives DISCONNECTED and its state becomes CLOSED.

How PPSSPP implements them (P2P mode)

Games named in this section are the ones PPSSPP's code gives as the reason for a behaviour. That those games need it hasn't been rechecked.

In P2P mode every PDP socket is a host UDP socket and every PTP socket a host TCP socket. In relay mode they're connections to the relay instead; see The relay. Both modes share the argument checking and the blocking emulation.

Ports

The host port is the PSP port plus the port offset setting (default 10000). Games often use low ports, which ordinary programs can't bind on Linux and Android, and the offset lifts them out of that range. All players must use the same offset, because the destination port of every send is offset the same way. A port that wraps around to 0 is mapped to 65535.

Port 0 ("any port") is passed to the OS as port 0, and the port the OS picked is mapped back by subtracting the offset.

Addresses

There's no ARP and no IP-level discovery. The only link between MAC and IP addresses is the peer list from the adhoc server:

  • Sending: the destination MAC is looked up in the peer list to get the peer's IP address. Sending to a MAC that isn't in the list returns 0 (success) and sends nothing.
  • Receiving: the sender's IP address is looked up to get its MAC. Datagrams from an IP address that isn't a known peer are dropped. The source port is the host port minus the port offset.
  • Accepting: as receiving. A connection from an unknown IP address is closed.
  • Two players behind the same public IP address can't be told apart: the first one in the list wins.

The local MAC is always written into the game's source MAC buffer by PdpCreate, PtpOpen and PtpListen, because some games pass a stale address from their save data.

PDP

CallPPSSPP
PdpCreateA UDP socket, non-blocking, with address and port reuse, bound to port + offset (to 127.0.0.N when using the built-in server on localhost, so instances on one machine don't collide). Send and receive buffers are 5 and 10 times bufsize. Requests a UPnP mapping if UPnP is on. A bind failure shows "Failed to Bind Port" on screen and returns 0x80410001. Only allowed once the game has scanned for, created or joined a group; before that it gives INVALID_ADDR.
PdpSend unicastOne UDP datagram to the peer's IP address and port + offset. Returns 0 on success, not the length.
PdpSend broadcastOne UDP datagram to each live peer in the list, then returns 0. The sender doesn't get a copy. There's no IP broadcast.
PdpRecvPeeks at the next datagram. If it's from an unknown IP address it's dropped. If it doesn't fit, NOT_ENOUGH_SPACE with the needed len, and the datagram stays queued. Otherwise it's read, and the sender's MAC and port are filled in. Returns 0 on success, not the length; some games (Kingdom Hearts Birth by Sleep) break otherwise.
GetPdpStatThe receive count is the number of bytes the OS has waiting, which can include more than one datagram.

PTP

PPSSPP sends nothing over the TCP connection except the game's data. The peer's MAC and port are worked out from the TCP connection's IP address and port.

CallPPSSPP
PtpOpenA TCP socket, non-blocking, with Nagle off, bound to port + offset, with keep-alive. Starts a non-blocking connect immediately, because some games (The Warriors) send without ever calling PtpConnect.
PtpConnectChecks the connect. If it succeeded: ESTABLISHED. If it's in progress: SYN_SENT, and a blocking call waits. If the other side refused (it isn't listening yet), the socket is recreated on the same port and the connect retried, since a TCP socket can't connect again after a refusal on most systems. Non-blocking calls return WOULD_BLOCK both while connecting and after a refusal, so P2P mode never reports CONNECTION_REFUSED.
PtpListenA TCP socket bound to port + offset, listen with the game's backlog. Requests a UPnP mapping.
PtpAcceptaccept, then maps the IP address to a MAC. The new socket inherits the listener's settings.
PtpSendsend. Allowed while still connecting, too. Partial sends report the length sent. An error other than "would block" closes the socket and returns DISCONNECTED.
PtpRecvrecv. A return of 0 bytes (the peer closed) or an error closes the socket and returns DISCONNECTED. This, send errors, and TCP keep-alive are how a vanished peer is detected.
PtpFlushAlways returns 0. Nagle is off, so data is never held back.
PtpCloseCloses the host socket.
GetPtpStatAlso notices completed connects, a while after the attempt (about two frames), because some games poll this instead of PtpConnect.

The retransmit arguments set the TCP keep-alive interval and count; TCP's own retransmission does the rest.

Polling

PollSocket runs one select over the host sockets: readable gives RECV (ACCEPT for a listener), writable gives SEND. For a PTP socket that's still connecting, CONNECT is reported once the connect has been under way for more than a millisecond, and DISCONNECT once it has closed after a connection attempt. Several games (Fate Unlimited Codes, Carnage Heart EXA, Bleach Heat the Soul 7) retry their connects based on these events.

The "Forced first connect" setting

With this on, the first PTP connect of a socket blocks until it succeeds or times out, and PtpOpen takes rexmt_int µs longer to return. This helps games that give up on a connection too quickly over a high-latency link (GvG Next Plus, Dissidia 012, Fate Unlimited Codes).

See Emulation details for how blocking calls are implemented and for timing, and Known issues for where this differs from the PSP.