From 7cb67b0b5dace44a9e02303affb4ff0a8ea6aa1c Mon Sep 17 00:00:00 2001 From: Ryan McClelland Date: Fri, 21 Aug 2026 14:27:01 -0700 Subject: [PATCH] libretro.h: add a link interface for machines joined by a cable A frontend that runs several cores at once has no way to let two of them reach each other. Each instance is generally loaded from its own copy of the shared library, so each has its own copy of every global and no in-core coordinator can be shared between them. The frontend is the only thing they have in common, so the frontend has to host the bus. That is enough to emulate the cable a great many machines shipped with: a Game Boy serial cable, a Game Boy Advance link cable, a GameCube-to-GBA cable, a PlayStation link cable. Today a core that wants this either does without, or emulates both machines itself inside one instance, which only works when both ends are the same core. This is not what RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE does. That joins separate frontend instances over a network and carries packets in real time, and it says so by disabling pause, rewind, fast forward and save state loading while a session is up. This joins core instances inside one process and carries payloads stamped on emulated time, with the frontend deciding how far each core may advance so that a transfer lands on the cycle it is due. The timing is the feature rather than an implementation detail: a link protocol delivered a millisecond late is a link protocol that has failed, which is why it cannot be built on a packet interface with no clock. The frontend never interprets a payload. All knowledge of the wire protocol stays in the cores, which is what lets two DIFFERENT cores emulating two different machines share one cable, and what keeps unrelated cores apart: peers whose protocol_id strings differ are never connected to each other. The grant returned by advance() is required to be a pure function of the participants' published ticks, with no wall-clock input and no timeout. A frontend that violates that desyncs any peer replaying the same inputs and expecting the same result, which is what rollback and lockstep multiplayer both do. The port handle follows the two frontend-hosted resources this header already carries. attach() returns an opaque retro_link_port_t * and every other call takes it, the way retro_vfs_open_t returns a retro_vfs_file_handle * and retro_open_mic_t a retro_microphone_t *. It names a PORT rather than a core, which is why it is needed at all: a machine with more than one socket, and a GameCube has four, has to say which one it means on every call. A frontend that does not implement this returns false, and a core that receives false behaves exactly as a machine with nothing in its socket, which such a core must already do. Cores and frontends can therefore adopt this independently of one another. Definitions only; no behaviour changes. --- libretro-common/include/libretro.h | 217 +++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/libretro-common/include/libretro.h b/libretro-common/include/libretro.h index 3def427e4fb..1dff1efe5cf 100644 --- a/libretro-common/include/libretro.h +++ b/libretro-common/include/libretro.h @@ -2882,6 +2882,223 @@ struct retro_memory_status uint64_t total; /**< Total physical memory installed. */ }; +/** + * Gives a core access to a link bus hosted by the frontend, so that two or more + * core instances running in the same process can emulate machines that were + * originally joined by a cable: a Game Boy serial cable, a Game Boy Advance + * link cable, a GameCube-to-GBA cable, a PlayStation link cable, and so on. + * + * This exists because core instances cannot reach each other on their own. A + * frontend that runs several cores at once generally loads each from its own + * copy of the shared library, so each instance has its own copy of every global + * and no in-core coordinator can be shared between them. The frontend is the + * only thing they have in common, so the frontend hosts the bus. + * + * Distinct from #RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE, which joins + * separate frontend instances over a network and carries packets in real time. + * This joins core instances inside one process and carries payloads stamped on + * emulated time, with the frontend deciding how far each core may advance so a + * transfer lands on the cycle it is due. The timing is the feature: a link + * protocol delivered a millisecond late is a link protocol that has failed, + * which is why this cannot be built on a packet interface with no clock. + * + * The frontend arbitrates emulated time and moves opaque payloads. All + * knowledge of the wire protocol stays in the cores; the frontend never + * interprets a payload, which is what allows two different cores emulating two + * different machines to share one cable. + * + * Should be called in \c retro_init or \c retro_load_game. The returned function + * pointers remain valid until \c retro_deinit, whether or not anything ever + * attaches, so a core has exactly one code path and still runs standalone: with + * nothing cabled to it, \c advance never bounds it and \c recv never yields, + * which is what a machine with an empty socket does anyway. + * + * @param[out] data struct retro_link_interface *. + * Set to the frontend's link interface. None of its members are \c NULL if this + * call returns \c true. + * @return \c true if the frontend hosts a link bus. + * \c false otherwise, which a core must treat as a socket with nothing in it — + * the behaviour it already has today. + * @see retro_link_interface + */ +#define RETRO_ENVIRONMENT_GET_LINK_INTERFACE (94 | RETRO_ENVIRONMENT_EXPERIMENTAL) + +/** + * Returned by \c retro_link_advance_t when nothing bounds this core: nothing is + * attached to the port, or every peer has detached. + * + * The core must then run freely. This is what lets a core that uses this + * interface behave exactly as it does today when no cable is plugged in. + */ +#define RETRO_LINK_UNBOUNDED ((uint64_t)-1) + +/** Peer id meaning "every other peer on this port's bus". */ +#define RETRO_LINK_BROADCAST 0xFF + +/** + * An attached port, as the frontend knows it. + * + * Handed back by \c retro_link_attach_t and passed to every other call, the way + * a file handle is by \c retro_vfs_open_t. The core never looks inside it. + * + * It names a PORT rather than a core, and that is what makes it necessary: a + * machine with more than one socket -- a GameCube has four -- must say which one + * it means on every call, so an argument of some kind was always going to be + * here. Naming the port with a token the frontend issued, rather than a bare + * number, lets the frontend validate it and tells it which core is calling for + * free. + * + * That second part is worth having. libretro's function pointers carry no + * userdata, so the usual way to recover the caller is a thread-local set when + * the core's emulation thread starts, which holds only while a core does its + * work on that one thread. Plenty do not: Dolphin runs its CPU on a thread of + * its own whenever dual-core is enabled, and that is a user-facing setting. + */ +typedef struct retro_link_port retro_link_port_t; + +/** + * Joins the bus on \c port. + * + * @param port The core's own port number. A machine with more than one socket — + * a GameCube has four — distinguishes them here. + * @param protocol_id Names the emulated wire protocol, e.g. "gba-sio-1". Peers + * whose ids differ are never connected to each other, which is what allows two + * different cores emulating two different machines to share one cable while + * keeping unrelated cores apart. + * @param clock_rate The number of ticks of this core's link timeline that elapse + * per second of emulated time, e.g. 16777216 for a Game Boy Advance, so the + * frontend can convert exactly between participants' units. Must not be zero. + * @return A handle to pass to every other call on this port, or \c NULL if it + * could not be attached. The core keeps it for as long as the port is attached + * and hands it back unchanged; nothing about its contents is a core's business. + * + * @note The bus index a core needs for addressing comes from + * \c retro_link_peers_t, not from here, because a cable can be plugged or pulled + * at any time and an index handed out once would be stale by the next transfer. + */ +typedef retro_link_port_t *(RETRO_CALLCONV *retro_link_attach_t)(unsigned port, + const char *protocol_id, uint64_t clock_rate); + +/** + * Leaves the bus. Peers observe this as a detach at the current tick. + * + * @param handle The handle returned by \c retro_link_attach_t. + */ +typedef void (RETRO_CALLCONV *retro_link_detach_t)(retro_link_port_t *handle); + +/** + * Current membership of this port's bus. + * + * Cores generally need this per transfer rather than once at attach time: the + * frontend decides what a port is cabled to, and a cable can be plugged or + * pulled while the machine is running, so a core's index and peer count can + * change during a session. + * + * @param handle The handle returned by \c retro_link_attach_t. + * @param[out] count If non-\c NULL, receives the number of participants, + * including this one. + * @return This core's index on the bus (0 or greater), or -1 when the port is + * not attached or is not cabled to anything. + */ +typedef int (RETRO_CALLCONV *retro_link_peers_t)(retro_link_port_t *handle, unsigned *count); + +/** + * Queues \c buf for delivery to a peer, stamped at \c tick on this core's + * timeline. + * + * The frontend copies the payload and does not interpret it. + * + * @param handle The handle returned by \c retro_link_attach_t. + * @param tick When the event happens, on this core's own timeline. + * @param to The peer's index on the bus, or #RETRO_LINK_BROADCAST. + * @param buf The payload. + * @param len The payload's length in bytes. + * @return \c false if the port is not attached. + */ +typedef bool (RETRO_CALLCONV *retro_link_send_t)(retro_link_port_t *handle, uint64_t tick, + unsigned to, const void *buf, size_t len); + +/** + * Pops the next queued message, oldest first, with its tick translated into this + * core's own units. + * + * The tick says when the event LANDS, not when the message became readable. A + * core is told about a transfer ahead of time precisely so it can schedule its + * own side of it for that tick, so messages are handed over as soon as they + * arrive rather than held until the clock catches up. What stops one landing in + * the past is the sender's commit horizon: it promised not to originate before + * that tick, and the bus will not have let this core run beyond it. + * + * @param handle The handle returned by \c retro_link_attach_t. + * @param[out] tick When the event lands, on this core's own timeline. + * @param[out] from The sender's index on the bus. + * @param[out] buf Receives the payload. + * @param[in,out] len Buffer capacity on entry, bytes written on return. + * @return \c false when the queue is empty. + */ +typedef bool (RETRO_CALLCONV *retro_link_recv_t)(retro_link_port_t *handle, uint64_t *tick, + unsigned *from, void *buf, size_t *len); + +/** + * Publishes this core's position and its commit horizon, then asks how far it + * may advance. Blocks until \c request_tick can be granted or until every peer + * has detached. + * + * The horizon is what keeps the bus deadlock-free, and it is required rather + * than an optimization. A core that always passes \c safe_tick equal to + * \c local_tick promises nothing, so if every participant does that they all sit + * at the same tick waiting for someone else to move and none of them ever can. + * Publishing a horizon some way ahead of the current position lets peers run to + * it without asking, and the core makes the promise true by scheduling anything + * it originates at the horizon rather than at the instant the guest asked for it. + * + * The horizon is therefore a tuning knob, traded against fidelity: a larger one + * means fewer rendezvous and more emulated delay before an originated event + * lands. Within a single process it only has to be large enough to avoid + * synchronizing every few cycles, so a small fraction of a frame is plenty. + * + * The grant MUST be a pure function of the participants' published ticks. It + * must never depend on wall-clock time, nor on the order in which threads happen + * to arrive. A frontend that violates this will desync any peer that replays the + * same inputs expecting the same result, which is exactly what rollback and + * lockstep multiplayer do. For the same reason there is deliberately no timeout: + * a peer that is behind is waited for, not guessed at. + * + * @param handle The handle returned by \c retro_link_attach_t. + * @param local_tick Where the core is now. + * @param safe_tick A promise that the core will not originate any event a peer + * must observe before that tick. Must be greater than or equal to + * \c local_tick and, once published, may never be retracted. + * @param request_tick How far the core is asking to advance. + * @return The tick this core may advance to: \c request_tick, or more only where + * a previous grant or the core's own position already stood further on, or + * #RETRO_LINK_UNBOUNDED. The grant is never larger than what was asked for even + * when more headroom happens to be available, because how far a peer has run at + * this instant is a wall-clock accident and returning it would make two machines + * replaying identical inputs disagree. Ask for more to get more. + * + * @note Thread-safe. Cores generally run on their own emulation threads, and + * this call is the rendezvous between them. + */ +typedef uint64_t (RETRO_CALLCONV *retro_link_advance_t)(retro_link_port_t *handle, + uint64_t local_tick, uint64_t safe_tick, uint64_t request_tick); + +/** + * Result of \c RETRO_ENVIRONMENT_GET_LINK_INTERFACE. + * + * All members are set by the frontend; none are \c NULL if the environment call + * returned \c true. + */ +struct retro_link_interface +{ + retro_link_attach_t attach; /**< Join the bus on a port. */ + retro_link_detach_t detach; /**< Leave the bus. */ + retro_link_peers_t peers; /**< Who else is on this port's bus. */ + retro_link_send_t send; /**< Queue a payload for a peer. */ + retro_link_recv_t recv; /**< Take the next payload off the queue. */ + retro_link_advance_t advance; /**< Publish a position, ask how far to run. */ +}; + /**@}*/ /**