diff --git a/libretro-common/include/libretro.h b/libretro-common/include/libretro.h index a08b69c0835..8d6a1664a49 100644 --- a/libretro-common/include/libretro.h +++ b/libretro-common/include/libretro.h @@ -1809,6 +1809,30 @@ enum retro_mod * even before the microphone driver is ready. */ +#define RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE 76 + /* const struct retro_netpacket_callback * -- + * When set, a core gains control over network packets sent and + * received during a multiplayer session. This can be used to + * emulate multiplayer games that were originally played on two + * or more separate consoles or computers connected together. + * + * The frontend will take care of connecting players together, + * and the core only needs to send the actual data as needed for + * the emulation, while handshake and connection management happen + * in the background. + * + * When two or more players are connected and this interface has + * been set, time manipulation features (such as pausing, slow motion, + * fast forward, rewinding, save state loading, etc.) are disabled to + * avoid interrupting communication. + * + * Should be set in either retro_init or retro_load_game, but not both. + * + * When not set, a frontend may use state serialization-based + * multiplayer, where a deterministic core supporting multiple + * input devices does not need to take any action on its own. + */ + /* VFS functionality */ /* File paths: @@ -3030,6 +3054,100 @@ struct retro_disk_control_ext_callback retro_get_image_label_t get_image_label; /* Optional - may be NULL */ }; +/* Definitions for RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE. + * A core can set it if sending and receiving custom network packets + * during a multiplayer session is desired. + */ + +/* Netpacket flags for retro_netpacket_send_t */ +#define RETRO_NETPACKET_UNRELIABLE 0 /* Packet to be sent unreliable, depending on network quality it might not arrive. */ +#define RETRO_NETPACKET_RELIABLE (1 << 0) /* Reliable packets are guaranteed to arrive at the target in the order they were send. */ +#define RETRO_NETPACKET_UNSEQUENCED (1 << 1) /* Packet will not be sequenced with other packets and may arrive out of order. Cannot be set on reliable packets. */ + +/* Used by the core to send a packet to one or more connected players. + * A single packet sent via this interface can contain up to 64 KB of data. + * + * The broadcast flag can be set to true to send to multiple connected clients. + * In a broadcast, the client_id argument indicates 1 client NOT to send the + * packet to (pass 0xFFFF to send to everyone). Otherwise, the client_id + * argument indicates a single client to send the packet to. + * + * A frontend must support sending reliable packets (RETRO_NETPACKET_RELIABLE). + * Unreliable packets might not be supported by the frontend, but the flags can + * still be specified. Reliable transmission will be used instead. + * + * If this function is called passing NULL for buf, it will instead flush all + * previously buffered outgoing packets and instantly read any incoming packets. + * During such a call, retro_netpacket_receive_t and retro_netpacket_stop_t can + * be called. The core can perform this in a loop to do a blocking read, i.e., + * wait for incoming data, but needs to handle stop getting called and also + * give up after a short while to avoid freezing on a connection problem. + * + * This function is not guaranteed to be thread-safe and must be called during + * retro_run or any of the netpacket callbacks passed with this interface. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_send_t)(int flags, const void* buf, size_t len, uint16_t client_id, bool broadcast); + +/* Called by the frontend to signify that a multiplayer session has started. + * If client_id is 0 the local player is the host of the session and at this + * point no other player has connected yet. + * + * If client_id is > 0 the local player is a client connected to a host and + * at this point is already fully connected to the host. + * + * The core must store the retro_netpacket_send_t function pointer provided + * here and use it whenever it wants to send a packet. This function pointer + * remains valid until the frontend calls retro_netpacket_stop_t. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_start_t)(uint16_t client_id, retro_netpacket_send_t send_fn); + +/* Called by the frontend when a new packet arrives which has been sent from + * another player with retro_netpacket_send_t. The client_id argument indicates + * who has sent the packet. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_receive_t)(const void* buf, size_t len, uint16_t client_id); + +/* Called by the frontend when the multiplayer session has ended. + * Once this gets called the retro_netpacket_send_t function pointer passed + * to retro_netpacket_start_t will not be valid anymore. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_stop_t)(void); + +/* Called by the frontend every frame (between calls to retro_run while + * updating the state of the multiplayer session. + * This is a good place for the core to call retro_netpacket_send_t from. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_poll_t)(void); + +/* Called by the frontend when a new player connects to the hosted session. + * This is only called on the host side, not for clients connected to the host. + * If this function returns false, the newly connected player gets dropped. + * This can be used for example to limit the number of players. + */ +typedef bool (RETRO_CALLCONV *retro_netpacket_connected_t)(uint16_t client_id); + +/* Called by the frontend when a player leaves or disconnects from the hosted session. + * This is only called on the host side, not for clients connected to the host. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_disconnected_t)(uint16_t client_id); + +/** + * A callback interface for giving a core the ability to send and receive custom + * network packets during a multiplayer session between two or more instances + * of a libretro frontend. + * + * @see RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE + */ +struct retro_netpacket_callback +{ + retro_netpacket_start_t start; + retro_netpacket_receive_t receive; + retro_netpacket_stop_t stop; /* Optional - may be NULL */ + retro_netpacket_poll_t poll; /* Optional - may be NULL */ + retro_netpacket_connected_t connected; /* Optional - may be NULL */ + retro_netpacket_disconnected_t disconnected; /* Optional - may be NULL */ +}; + enum retro_pixel_format { /* 0RGB1555, native endian. diff --git a/network/netplay/netplay.h b/network/netplay/netplay.h index 0bd1eb6edbe..48e146fe778 100644 --- a/network/netplay/netplay.h +++ b/network/netplay/netplay.h @@ -147,6 +147,7 @@ typedef struct struct netplay_room host_room; struct netplay_room *room_list; struct netplay_rooms *rooms_data; + struct retro_netpacket_callback *core_netpacket_interface; /* Used while Netplay is running */ netplay_t *data; netplay_client_info_t *client_info; diff --git a/network/netplay/netplay_defines.h b/network/netplay/netplay_defines.h index a52021480c0..6c8c78340b8 100644 --- a/network/netplay/netplay_defines.h +++ b/network/netplay/netplay_defines.h @@ -74,7 +74,10 @@ enum rarch_netplay_ctl_state RARCH_NETPLAY_CTL_DESYNC_PUSH, RARCH_NETPLAY_CTL_DESYNC_POP, RARCH_NETPLAY_CTL_KICK_CLIENT, - RARCH_NETPLAY_CTL_BAN_CLIENT + RARCH_NETPLAY_CTL_BAN_CLIENT, + RARCH_NETPLAY_CTL_SET_CORE_PACKET_INTERFACE, + RARCH_NETPLAY_CTL_SKIP_NETPLAY_CALLBACKS, + RARCH_NETPLAY_CTL_ALLOW_TIMESKIP }; /* The current status of a connection */ diff --git a/network/netplay/netplay_frontend.c b/network/netplay/netplay_frontend.c index b45b64b9a65..1b1dda3c28f 100644 --- a/network/netplay/netplay_frontend.c +++ b/network/netplay/netplay_frontend.c @@ -675,6 +675,11 @@ static uint32_t simple_rand_uint32(unsigned long *simple_rand_next) return ((part0 << 30) + (part1 << 15) + part2); } +static void netplay_send_cmd_netpacket(netplay_t *netplay, size_t conn_i, + const void* buf, size_t len, uint16_t client_id, bool broadcast); +static void RETRO_CALLCONV netplay_netpacket_send_cb(int flags, + const void* buf, size_t len, uint16_t client_id, bool broadcast); + /* * netplay_init_socket_buffer * @@ -1876,6 +1881,12 @@ static bool netplay_handshake_pre_sync(netplay_t *netplay, netplay->next_ping = cpu_features_get_time_usec() + NETPLAY_PING_AFTER; + /* Tell a core that uses the netpacket interface that the client is ready */ + if (networking_driver_st.core_netpacket_interface && + networking_driver_st.core_netpacket_interface->start) + networking_driver_st.core_netpacket_interface->start + ((uint16_t)netplay->self_client_num, netplay_netpacket_send_cb); + /* Ask to switch to playing mode if we should */ if (!settings->bools.netplay_start_as_spectator) return netplay_cmd_mode(netplay, NETPLAY_CONNECTION_PLAYING); @@ -1915,8 +1926,9 @@ static bool netplay_handshake(netplay_t *netplay, return false; } - if (connection->mode >= NETPLAY_CONNECTION_CONNECTED && - !netplay_send_cur_input(netplay, connection)) + if (connection->mode >= NETPLAY_CONNECTION_CONNECTED + && netplay->modus == NETPLAY_MODUS_INPUT_FRAME_SYNC + && !netplay_send_cur_input(netplay, connection)) return false; return ret; @@ -3541,7 +3553,8 @@ static bool netplay_sync_pre_frame(netplay_t *netplay) bool ret = true; if (netplay->run_frame_count > 0 && netplay_delta_frame_ready(netplay, - &netplay->buffer[netplay->run_ptr], netplay->run_frame_count)) + &netplay->buffer[netplay->run_ptr], netplay->run_frame_count) + && netplay->modus == NETPLAY_MODUS_INPUT_FRAME_SYNC) { /* Don't serialize until it's safe. */ if (!(netplay->quirks & NETPLAY_QUIRK_INITIALIZATION)) @@ -3653,14 +3666,14 @@ static bool netplay_sync_pre_frame(netplay_t *netplay) } /** - * netplay_sync_post_frame + * netplay_sync_input_post_frame * @netplay : pointer to netplay object * @stalled : true if we're currently stalled * * Post-frame for Netplay synchronization. * We check if we have new input and replay from recorded input. */ -static void netplay_sync_post_frame(netplay_t *netplay, bool stalled) +static void netplay_sync_input_post_frame(netplay_t *netplay, bool stalled) { uint32_t lo_frame_count, hi_frame_count; @@ -3748,8 +3761,9 @@ static void netplay_sync_post_frame(netplay_t *netplay, bool stalled) #endif /* Now replay the real input if we've gotten ahead of it */ - if (netplay->force_rewind || + if ((netplay->force_rewind || netplay->replay_frame_count < netplay->run_frame_count) + && netplay->modus == NETPLAY_MODUS_INPUT_FRAME_SYNC) { retro_ctx_serialize_info_t serial_info; @@ -4067,6 +4081,12 @@ static void netplay_hangup(netplay_t *netplay, #endif } + if (networking_driver_st.core_netpacket_interface + && was_playing && netplay->is_server + && networking_driver_st.core_netpacket_interface->disconnected) + networking_driver_st.core_netpacket_interface->disconnected + ((uint16_t)(connection - netplay->connections + 1)); + RARCH_LOG("[Netplay] %s\n", dmsg); /* This notification is really only important to the server if the client was playing. * Let it be optional if server and the client wasn't playing. */ @@ -4431,7 +4451,7 @@ static uint8_t netplay_settings_share_mode( static void announce_play_spectate(netplay_t *netplay, const char *nick, enum rarch_netplay_connection_mode mode, uint32_t devices, - int32_t ping) + int32_t ping, uint32_t client_num) { char msg[512]; const char *dmsg = NULL; @@ -4458,16 +4478,23 @@ static void announce_play_spectate(netplay_t *netplay, uint32_t one_device = (uint32_t) -1; char *pdevice_str = NULL; - for (device = 0; device < MAX_INPUT_DEVICES; device++) + if (netplay->modus == NETPLAY_MODUS_CORE_PACKET_INTERFACE) { - if (!(devices & (1<nick, - NETPLAY_CONNECTION_SPECTATING, 0, -1); + NETPLAY_CONNECTION_SPECTATING, 0, -1, client_num); } else { @@ -4589,7 +4616,7 @@ static void handle_play_spectate(netplay_t *netplay, netplay->self_mode = NETPLAY_CONNECTION_SPECTATING; announce_play_spectate(netplay, NULL, - NETPLAY_CONNECTION_SPECTATING, 0, -1); + NETPLAY_CONNECTION_SPECTATING, 0, -1, client_num); /* It was the server, so tell everyone else */ netplay_send_raw_cmd_all(netplay, NULL, @@ -4623,7 +4650,12 @@ static void handle_play_spectate(netplay_t *netplay, share_mode &= ~NETPLAY_SHARE_NO_PREFERENCE; } - if (devices) + if (netplay->modus == NETPLAY_MODUS_CORE_PACKET_INTERFACE) + { + /* no device needs to be assigned with netpacket interface */ + devices = 0; + } + else if (devices) { /* Make sure the devices are available and/or shareable */ for (i = 0; i < MAX_INPUT_DEVICES; i++) @@ -4749,7 +4781,10 @@ static void handle_play_spectate(netplay_t *netplay, if (settings->bools.netplay_allow_slaves) { - if (settings->bools.netplay_require_slaves) + /* Slave mode unused when core uses netpacket interface */ + if (netplay->modus == NETPLAY_MODUS_CORE_PACKET_INTERFACE) + slave = false; + else if (settings->bools.netplay_require_slaves) slave = true; else slave = (mode & NETPLAY_CMD_PLAY_BIT_SLAVE) ? @@ -4777,12 +4812,27 @@ static void handle_play_spectate(netplay_t *netplay, payload.mode = htonl(mode | NETPLAY_CMD_MODE_BIT_YOU); + if (networking_driver_st.core_netpacket_interface + && networking_driver_st.core_netpacket_interface->connected + && !networking_driver_st.core_netpacket_interface->connected( + (uint16_t)(connection - netplay->connections + 1))) + { + /* core wants us to drop this new client */ + uint32_t reason = htonl( + NETPLAY_CMD_MODE_REFUSED_REASON_OTHER); + connection->mode = NETPLAY_CONNECTION_CONNECTED; + netplay_send_raw_cmd(netplay, connection, + NETPLAY_CMD_MODE_REFUSED, &reason, sizeof(reason)); + netplay_hangup(netplay, connection); + return; + } + /* Tell the player */ netplay_send_raw_cmd(netplay, connection, NETPLAY_CMD_MODE, &payload, sizeof(payload)); announce_play_spectate(netplay, connection->nick, - connection->mode, devices, connection->ping); + connection->mode, devices, connection->ping, client_num); } else { @@ -4800,7 +4850,7 @@ static void handle_play_spectate(netplay_t *netplay, netplay->self_mode = NETPLAY_CONNECTION_PLAYING; announce_play_spectate(netplay, NULL, - netplay->self_mode, devices, -1); + netplay->self_mode, devices, -1, client_num); } payload.mode = htonl(mode); @@ -5511,7 +5561,9 @@ static bool netplay_get_cmd(netplay_t *netplay, /* A change to me! */ if (mode & NETPLAY_CMD_MODE_BIT_PLAYING) { - if (frame != netplay->server_frame_count) + /* When a core uses the netpacket interface this is OK */ + if (frame != netplay->server_frame_count + && netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) { RARCH_ERR("[Netplay] Received mode change out of order.\n"); return netplay_cmd_nak(netplay, connection); @@ -5540,66 +5592,69 @@ static bool netplay_get_cmd(netplay_t *netplay, netplay->read_ptr[client_num] = netplay->server_ptr; netplay->read_frame_count[client_num] = netplay->server_frame_count; - /* Fix up current frame info */ - if (frame <= netplay->self_frame_count && - netplay->self_mode == NETPLAY_CONNECTION_PLAYING) + if (netplay->modus == NETPLAY_MODUS_INPUT_FRAME_SYNC) { - /* It wanted past frames, better send 'em! */ - START(netplay->server_ptr); - while (dframe->used && dframe->frame <= netplay->self_frame_count) + /* Fix up current frame info */ + if (frame <= netplay->self_frame_count && + netplay->self_mode == NETPLAY_CONNECTION_PLAYING) { - for (device = 0; device < MAX_INPUT_DEVICES; device++) + /* It wanted past frames, better send 'em! */ + START(netplay->server_ptr); + while (dframe->used && dframe->frame <= netplay->self_frame_count) { - uint32_t dsize; - netplay_input_state_t istate; - if (!(devices & (1<real_input[device], client_num, dsize, - false, false); - if (!istate) - continue; - memset(istate->data, 0, dsize*sizeof(uint32_t)); + for (device = 0; device < MAX_INPUT_DEVICES; device++) + { + uint32_t dsize; + netplay_input_state_t istate; + if (!(devices & (1<real_input[device], client_num, dsize, + false, false); + if (!istate) + continue; + memset(istate->data, 0, dsize*sizeof(uint32_t)); + } + dframe->have_local = true; + dframe->have_real[client_num] = true; + send_input_frame(netplay, dframe, connection, NULL, client_num, false); + if (dframe->frame == netplay->self_frame_count) break; + NEXT(); } - dframe->have_local = true; - dframe->have_real[client_num] = true; - send_input_frame(netplay, dframe, connection, NULL, client_num, false); - if (dframe->frame == netplay->self_frame_count) break; - NEXT(); } - } - else - { - uint32_t frame_count; + else + { + uint32_t frame_count; - /* It wants future frames, make sure we don't capture or send intermediate ones */ - START(netplay->self_ptr); - frame_count = netplay->self_frame_count; + /* It wants future frames, make sure we don't capture or send intermediate ones */ + START(netplay->self_ptr); + frame_count = netplay->self_frame_count; - do - { - if (!dframe->used) + do { - /* Make sure it's ready */ - if (!netplay_delta_frame_ready(netplay, dframe, frame_count)) + if (!dframe->used) { - RARCH_ERR("[Netplay] Received mode change but delta frame isn't ready!\n"); - return netplay_cmd_nak(netplay, connection); + /* Make sure it's ready */ + if (!netplay_delta_frame_ready(netplay, dframe, frame_count)) + { + RARCH_ERR("[Netplay] Received mode change but delta frame isn't ready!\n"); + return netplay_cmd_nak(netplay, connection); + } } - } - dframe->have_local = true; + dframe->have_local = true; - /* Go on to the next delta frame */ - NEXT(); - frame_count++; - } while (frame_count < frame); + /* Go on to the next delta frame */ + NEXT(); + frame_count++; + } while (frame_count < frame); + } } /* Announce it */ announce_play_spectate(netplay, NULL, netplay->self_mode, devices, - connection->ping); + connection->ping, client_num); #ifdef DEBUG_NETPLAY_STEPS RARCH_LOG("[Netplay] Received mode change self->%X\n", devices); @@ -5626,7 +5681,9 @@ static bool netplay_get_cmd(netplay_t *netplay, /* Somebody else is joining or parting */ if (mode & NETPLAY_CMD_MODE_BIT_PLAYING) { - if (frame != netplay->server_frame_count) + /* When a core uses the netpacket interface this is OK */ + if (frame != netplay->server_frame_count + && netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) { RARCH_ERR("[Netplay] Received mode change out of order.\n"); return netplay_cmd_nak(netplay, connection); @@ -5642,7 +5699,7 @@ static bool netplay_get_cmd(netplay_t *netplay, netplay->read_frame_count[client_num] = netplay->server_frame_count; /* Announce it */ - announce_play_spectate(netplay, nick, NETPLAY_CONNECTION_PLAYING, devices, -1); + announce_play_spectate(netplay, nick, NETPLAY_CONNECTION_PLAYING, devices, -1, client_num); #ifdef DEBUG_NETPLAY_STEPS RARCH_LOG("[Netplay] Received mode change %u->%u\n", client_num, devices); @@ -5657,7 +5714,7 @@ static bool netplay_get_cmd(netplay_t *netplay, netplay->device_clients[device] &= ~(1<spectator\n", client_num); @@ -6126,6 +6183,66 @@ static bool netplay_get_cmd(netplay_t *netplay, break; } + case NETPLAY_CMD_NETPACKET: + case NETPLAY_CMD_NETPACKET_BROADCAST: + { + uint32_t pkt_client_id; + void* buf = netplay->zbuffer; + if (!networking_driver_st.core_netpacket_interface) + { + RARCH_ERR("[Netplay] NETPLAY_CMD_NETPACKET while core netpacket interface is not set.\n"); + return netplay_cmd_nak(netplay, connection); + } + if (cmd_size > netplay->zbuffer_size) + { + RARCH_ERR("[Netplay] Received netpacket of unexpected size.\n"); + return netplay_cmd_nak(netplay, connection); + } + + RECV(&pkt_client_id, sizeof(pkt_client_id)) + return false; + pkt_client_id = ntohl(pkt_client_id); + RECV(buf, cmd_size) + return false; + + if (!netplay->is_server) + { + /* packets arriving at a client are always meant for us */ + if (networking_driver_st.core_netpacket_interface->receive) + networking_driver_st.core_netpacket_interface->receive + (buf, cmd_size, (uint16_t)pkt_client_id); + } + else + { + bool broadcast = (cmd == NETPLAY_CMD_NETPACKET_BROADCAST); + uint16_t incoming_client_id = + (uint16_t)(connection - netplay->connections + 1); + + /* check if this is a packet for the host */ + if ((broadcast ? pkt_client_id : !pkt_client_id) + && networking_driver_st.core_netpacket_interface->receive) + networking_driver_st.core_netpacket_interface->receive + (buf, cmd_size, incoming_client_id); + + if (broadcast) + { + /* relay to all but designated client and incoming client */ + size_t i, skip1 = pkt_client_id, skip2 = incoming_client_id; + for (i = 0; i < netplay->connections_size; i++) + if (i+1 != skip1 && i+1 != skip2) + netplay_send_cmd_netpacket(netplay, i, + buf, cmd_size, incoming_client_id, false); + } + else if (pkt_client_id && pkt_client_id != incoming_client_id) + { + /* relay unless target is the host or the incoming client */ + netplay_send_cmd_netpacket(netplay, pkt_client_id-1, + buf, cmd_size, incoming_client_id, false); + } + } + break; + } + case NETPLAY_CMD_PLAYER_CHAT: { char nickname[NETPLAY_NICK_LEN]; @@ -6742,15 +6859,23 @@ static bool netplay_init_socket_buffers(netplay_t *netplay) static bool netplay_init_serialization(netplay_t *netplay) { - size_t i, info_size; + size_t i; if (netplay->state_size) return true; - info_size = core_serialize_size_special(); - if (!info_size) - return false; - netplay->state_size = info_size; + if (netplay->modus == NETPLAY_MODUS_CORE_PACKET_INTERFACE) + { + /* max core netpacket size is 64 kb, hold 2 of them */ + netplay->state_size = 64*1024*2; + } + else + { + size_t info_size = core_serialize_size_special(); + if (!info_size) + return false; + netplay->state_size = info_size; + } for (i = 0; i < netplay->buffer_size; i++) { @@ -6947,7 +7072,8 @@ static void netplay_free(netplay_t *netplay) static netplay_t *netplay_new(const char *server, const char *mitm, uint16_t port, const char *mitm_session, uint32_t check_frames, const struct retro_callbacks *cb, - bool nat_traversal, const char *nick, uint32_t quirks) + bool nat_traversal, const char *nick, uint32_t quirks, + enum netplay_modus modus) { netplay_t *netplay = (netplay_t*)calloc(1, sizeof(*netplay)); @@ -6958,6 +7084,7 @@ static netplay_t *netplay_new(const char *server, const char *mitm, netplay->check_frames = check_frames; netplay->cbs = *cb; netplay->quirks = quirks; + netplay->modus = modus; netplay->crcs_valid = true; netplay->listen_fd = -1; netplay->next_announce = -1; @@ -7127,6 +7254,12 @@ static void netplay_frontend_paused(netplay_t *netplay, bool paused) size_t i; uint32_t paused_ct = 0; + /* When a core uses the netpacket interface netplay doesn't control pause. + * We need this because even if RARCH_NETPLAY_CTL_ALLOW_PAUSE returns false + * on some platforms the frontend may try to force netplay to pause. */ + if (netplay->modus == NETPLAY_MODUS_CORE_PACKET_INTERFACE) + return; + netplay->local_paused = paused; /* Communicating this is a bit odd: If exactly one other connection is @@ -7307,7 +7440,8 @@ static void netplay_toggle_play_spectate(netplay_t *netplay) if (!netplay->is_server) { int i; - uint32_t client_mask = ~(1 << netplay->self_client_num); + uint32_t client_num = netplay->self_client_num; + uint32_t client_mask = ~(1 << client_num); netplay->connected_players &= client_mask; @@ -7319,7 +7453,7 @@ static void netplay_toggle_play_spectate(netplay_t *netplay) netplay->self_mode = NETPLAY_CONNECTION_SPECTATING; announce_play_spectate(netplay, NULL, - NETPLAY_CONNECTION_SPECTATING, 0, -1); + NETPLAY_CONNECTION_SPECTATING, 0, -1, client_num); } netplay_cmd_mode(netplay, NETPLAY_CONNECTION_SPECTATING); @@ -7581,6 +7715,26 @@ static bool netplay_poll(netplay_t *netplay, bool block_libretro_input) { size_t i; + /* Use simplified polling loop when a core uses the netpacket interface. */ + if (netplay->modus == NETPLAY_MODUS_CORE_PACKET_INTERFACE) + { + if (netplay->self_mode == NETPLAY_CONNECTION_NONE) + return true; + + /* Read netplay input. */ + netplay_poll_net_input(netplay); + + /* Handle any delayed state changes */ + if (netplay->is_server) + netplay_delayed_state_change(netplay); + + if (networking_driver_st.core_netpacket_interface + && networking_driver_st.core_netpacket_interface->poll + && netplay->self_mode == NETPLAY_CONNECTION_PLAYING) + networking_driver_st.core_netpacket_interface->poll(); + return true; + } + if (!get_self_input_state(block_libretro_input, netplay)) goto catastrophe; @@ -8375,12 +8529,13 @@ static bool netplay_pre_frame(netplay_t *netplay) } } - if ((netplay->stall || netplay->remote_paused) && - (!netplay->is_server || netplay->connected_players > 1)) + if ((netplay->stall || netplay->remote_paused) + && (!netplay->is_server || netplay->connected_players > 1) + && netplay->modus == NETPLAY_MODUS_INPUT_FRAME_SYNC) { /* We may have received data even if we're stalled, * so run post-frame sync. */ - netplay_sync_post_frame(netplay, true); + netplay_sync_input_post_frame(netplay, true); return false; } @@ -8399,8 +8554,12 @@ static void netplay_post_frame(netplay_t *netplay) { size_t i; - netplay_update_unread_ptr(netplay); - netplay_sync_post_frame(netplay, false); + /* When a core uses the netpacket interface frames are not synced */ + if (netplay->modus == NETPLAY_MODUS_INPUT_FRAME_SYNC) + { + netplay_update_unread_ptr(netplay); + netplay_sync_input_post_frame(netplay, false); + } for (i = 0; i < netplay->connections_size; i++) { @@ -8442,6 +8601,10 @@ void deinit_netplay(void) /* Reinitialize preemptive frames if enabled */ preempt_init(runloop_state_get_ptr()); #endif + + if (net_st->core_netpacket_interface + && net_st->core_netpacket_interface->stop) + net_st->core_netpacket_interface->stop(); } free(net_st->client_info); @@ -8461,6 +8624,7 @@ bool init_netplay(const char *server, unsigned port, const char *mitm_session) net_driver_state_t *net_st = &networking_driver_st; struct netplay_room *host_room = &net_st->host_room; const char *mitm = NULL; + enum netplay_modus modus = NETPLAY_MODUS_CORE_PACKET_INTERFACE; if (!(net_st->flags & NET_DRIVER_ST_FLAG_NETPLAY_ENABLED)) return false; @@ -8471,9 +8635,13 @@ bool init_netplay(const char *server, unsigned port, const char *mitm_session) serialization_quirks = core_serialization_quirks(); - if (!core_info_current_supports_netplay() || + if (net_st->core_netpacket_interface) + modus = NETPLAY_MODUS_CORE_PACKET_INTERFACE; + + if ((!core_info_current_supports_netplay() || serialization_quirks & (RETRO_SERIALIZATION_QUIRK_INCOMPLETE | RETRO_SERIALIZATION_QUIRK_SINGLE_SESSION)) + && modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) { RARCH_ERR("[Netplay] %s\n", msg_hash_to_str(MSG_NETPLAY_UNSUPPORTED)); runloop_msg_queue_push( @@ -8544,7 +8712,7 @@ bool init_netplay(const char *server, unsigned port, const char *mitm_session) : #endif settings->paths.username, - quirks); + quirks, modus); if (!netplay) goto failure; @@ -8593,6 +8761,11 @@ bool init_netplay(const char *server, unsigned port, const char *mitm_session) netplay->self_mode = NETPLAY_CONNECTION_INIT; } + /* Tell a core that uses the netpacket interface that the host is ready */ + if (netplay->is_server && net_st->core_netpacket_interface && + net_st->core_netpacket_interface->start) + net_st->core_netpacket_interface->start(0, netplay_netpacket_send_cb); + return true; failure: @@ -8748,6 +8921,18 @@ static bool kick_client_by_id_and_name(netplay_t *netplay, return true; } +static bool netplay_have_any_active_connection(netplay_t *netplay) +{ + size_t i; + if (!netplay || !netplay->is_server) + return (netplay && netplay->self_mode >= NETPLAY_CONNECTION_CONNECTED); + for (i = 0; i < netplay->connections_size; i++) + if ( (netplay->connections[i].flags & NETPLAY_CONN_FLAG_ACTIVE) + && (netplay->connections[i].mode >= NETPLAY_CONNECTION_CONNECTED)) + return true; + return false; +} + /** * netplay_driver_ctl * @@ -8915,7 +9100,7 @@ bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data) break; case RARCH_NETPLAY_CTL_GAME_WATCH: - if (netplay) + if (netplay && netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) netplay_toggle_play_spectate(netplay); else ret = false; @@ -8929,7 +9114,9 @@ bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data) break; case RARCH_NETPLAY_CTL_ALLOW_PAUSE: - ret = (!netplay || netplay->allow_pausing); + ret = (!netplay || (netplay->allow_pausing && + (netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE || + !netplay_have_any_active_connection(netplay)))); break; case RARCH_NETPLAY_CTL_PAUSE: @@ -8944,13 +9131,13 @@ bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data) break; case RARCH_NETPLAY_CTL_LOAD_SAVESTATE: - if (netplay) + if (netplay && netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) netplay_load_savestate(netplay, (retro_ctx_serialize_info_t*)data, true); break; case RARCH_NETPLAY_CTL_RESET: - if (netplay) + if (netplay && netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) netplay_core_reset(netplay); break; @@ -8968,7 +9155,12 @@ bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data) case RARCH_NETPLAY_CTL_DESYNC_PUSH: if (netplay) - netplay->desync++; + { + if (netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE) + netplay->desync++; + else if (netplay_have_any_active_connection(netplay)) + ret = false; + } break; case RARCH_NETPLAY_CTL_DESYNC_POP: @@ -9027,6 +9219,47 @@ bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data) ret = false; break; + case RARCH_NETPLAY_CTL_SET_CORE_PACKET_INTERFACE: + if (!net_st->core_netpacket_interface && data && netplay) + { + /* it's too late to enable the interface */ + ret = false; + break; + } + + if (net_st->core_netpacket_interface) + { + if (data && !memcmp(net_st->core_netpacket_interface, + data, sizeof(*net_st->core_netpacket_interface))) + break; /* interface is not modified */ + + free(net_st->core_netpacket_interface); + net_st->core_netpacket_interface = NULL; + } + + if (data) + { + /* copy passed interface to local state */ + net_st->core_netpacket_interface = (struct retro_netpacket_callback*) + malloc(sizeof(*net_st->core_netpacket_interface)); + *net_st->core_netpacket_interface = *(struct retro_netpacket_callback*)data; + /* reset savefile dir as core_netpacket_interface affects it */ + runloop_path_set_redirect(config_get_ptr(), + dir_get_ptr(RARCH_DIR_SAVEFILE), + dir_get_ptr(RARCH_DIR_CURRENT_SAVESTATE)); + } + break; + + case RARCH_NETPLAY_CTL_SKIP_NETPLAY_CALLBACKS: + ret = (net_st->core_netpacket_interface != NULL); + break; + + case RARCH_NETPLAY_CTL_ALLOW_TIMESKIP: + ret = (!netplay + || netplay->modus != NETPLAY_MODUS_CORE_PACKET_INTERFACE + || !netplay_have_any_active_connection(netplay)); + break; + case RARCH_NETPLAY_CTL_NONE: default: ret = false; @@ -9122,6 +9355,79 @@ bool netplay_decode_hostname(const char *hostname, return true; } +/** + * netplay_send_cmd_netpacket + * @netplay : pointer to netplay object + * @conn_idx : connection index to send cmd to + * @buf : packet data pointer + * @len : packet data size + * @pkt_client_id : source id if host sending to client, otherwise recipient id or excepted id if broadcast + * @broadcast : pass true on client if host should relay this to everyone else + * + * Send a netpacket command to a connected peer. + */ +static void netplay_send_cmd_netpacket(netplay_t *netplay, size_t conn_idx, + const void* buf, size_t len, uint16_t pkt_client_id, bool broadcast) +{ + struct netplay_connection *connection; + struct socket_buffer *sbuf; + uint32_t cmdbuf[3]; + bool need_flush; + + if (conn_idx >= netplay->connections_size) return; + connection = &netplay->connections[conn_idx]; + if (!(connection->flags & NETPLAY_CONN_FLAG_ACTIVE)) return; + if (connection->mode != NETPLAY_CONNECTION_PLAYING) return; + + cmdbuf[0] = htonl(broadcast + ? NETPLAY_CMD_NETPACKET_BROADCAST : NETPLAY_CMD_NETPACKET); + cmdbuf[1] = htonl(len); + cmdbuf[2] = htonl(pkt_client_id); + + sbuf = &connection->send_packet_buffer; + need_flush = (buf_remaining(sbuf) < sizeof(cmdbuf)+len); + + if ( (need_flush && !netplay_send_flush(sbuf, connection->fd, true)) + || (!netplay_send(sbuf, connection->fd, cmdbuf, sizeof(cmdbuf))) + || (len && !netplay_send(sbuf, connection->fd, buf, len))) + netplay_hangup(netplay, connection); +} + +static void RETRO_CALLCONV netplay_netpacket_send_cb(int flags, + const void* buf, size_t len, uint16_t client_id, bool broadcast) +{ + net_driver_state_t *net_st = &networking_driver_st; + netplay_t *netplay = net_st->data; + if (!netplay) return; + + if (buf == NULL) + { + /* With NULL this function instead flushes packets and checks incoming */ + netplay_send_flush_all(netplay, NULL); + input_poll_net(netplay); + return; + } + + if (!netplay->is_server) + { + /* client always sends packet to host, host will relay it if needed */ + netplay_send_cmd_netpacket(netplay, 0, buf, len, client_id, broadcast); + } + else if (broadcast) + { + /* send packet to all clients (client_id can be set as exception) */ + size_t i; + for (i = 0; i < netplay->connections_size; i++) + if (i+1 != client_id) + netplay_send_cmd_netpacket(netplay, i, buf, len, 0, false); + } + else if (client_id) + { + /* send packet to specific client */ + netplay_send_cmd_netpacket(netplay, client_id-1, buf, len, 0, false); + } +} + /* Netplay Widgets */ #ifdef HAVE_GFX_WIDGETS diff --git a/network/netplay/netplay_private.h b/network/netplay/netplay_private.h index 781d30bbabb..37bd925f532 100644 --- a/network/netplay/netplay_private.h +++ b/network/netplay/netplay_private.h @@ -162,6 +162,12 @@ enum netplay_cmd /* Sends over cheats enabled on client (unsupported) */ NETPLAY_CMD_CHEATS = 0x0047, + /* Send a network packet from the raw packet core interface */ + NETPLAY_CMD_NETPACKET = 0x0048, + + /* Used by clients to have the host also forward it to other clients */ + NETPLAY_CMD_NETPACKET_BROADCAST = 0x0049, + /* Misc. commands */ /* Sends multiple config requests over, @@ -265,6 +271,19 @@ enum rarch_netplay_stall_reason NETPLAY_STALL_SERVER_REQUESTED }; +enum netplay_modus +{ + /* Netplay operates by having all participants send input data every + frame and run cores deterministically in sync on all connected devices. + It will rewind frames when input data from the past arrives. */ + NETPLAY_MODUS_INPUT_FRAME_SYNC = 0, + + /* Netplay operates by having the active core send and receive custom + packets once connection setup and handshake has been completed. + Time skips (pausing, fast forward, save state loading) are refused. */ + NETPLAY_MODUS_CORE_PACKET_INTERFACE = 1 +}; + /* Input state for a particular client-device pair */ typedef struct netplay_input_state { @@ -604,6 +623,9 @@ struct netplay /* Are we stalled? */ enum rarch_netplay_stall_reason stall; + /* Netplay mode of operation (cannot change at runtime) */ + enum netplay_modus modus; + /* Keyboard mapping (network and host) */ uint16_t mapping_hton[RETROK_LAST]; uint16_t mapping_ntoh[NETPLAY_KEY_LAST]; diff --git a/retroarch.c b/retroarch.c index 68deb945109..0ddaa4e34c6 100644 --- a/retroarch.c +++ b/retroarch.c @@ -2520,6 +2520,10 @@ bool command_event(enum event_command cmd, void *data) runloop_msg_queue_push(msg_hash_to_str(MSG_CHEEVOS_LOAD_STATE_PREVENTED_BY_HARDCORE_MODE), 0, 180, true, NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_WARNING); return false; } +#endif +#ifdef HAVE_NETWORKING + if (!netplay_driver_ctl(RARCH_NETPLAY_CTL_ALLOW_TIMESKIP, NULL)) + return false; #endif if (!command_event_main_state(cmd)) return false; @@ -2903,6 +2907,10 @@ bool command_event(enum event_command cmd, void *data) #ifdef HAVE_CHEEVOS if (rcheevos_hardcore_active()) return false; +#endif +#ifdef HAVE_NETWORKING + if (!netplay_driver_ctl(RARCH_NETPLAY_CTL_ALLOW_TIMESKIP, NULL)) + return false; #endif if (rewind_enable) { diff --git a/runloop.c b/runloop.c index 4603c60cc86..8f2e14c2a76 100644 --- a/runloop.c +++ b/runloop.c @@ -3484,6 +3484,19 @@ bool runloop_environment_cb(unsigned cmd, void *data) } break; + case RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE: +#ifdef HAVE_NETWORKING + RARCH_LOG("[Environ]: RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE.\n"); + if (!netplay_driver_ctl(RARCH_NETPLAY_CTL_SET_CORE_PACKET_INTERFACE, data)) + { + RARCH_ERR("[Environ] RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE set too late\n"); + return false; + } + break; +#else + return false; +#endif + default: RARCH_LOG("[Environ]: UNSUPPORTED (#%u).\n", cmd); return false; @@ -4032,6 +4045,9 @@ void runloop_event_deinit_core(void) #if defined(HAVE_CG) || defined(HAVE_GLSL) || defined(HAVE_SLANG) || defined(HAVE_HLSL) runloop_st->runtime_shader_preset_path[0] = '\0'; #endif +#ifdef HAVE_NETWORKING + netplay_driver_ctl(RARCH_NETPLAY_CTL_SET_CORE_PACKET_INTERFACE, NULL); +#endif } static bool runloop_path_init_subsystem(runloop_state_t *runloop_st) @@ -6310,6 +6326,12 @@ static enum runloop_state_enum runloop_check_state( input_st->flags |= INP_FLAG_NONBLOCKING; } +#ifdef HAVE_NETWORKING + if (check2 + && !netplay_driver_ctl(RARCH_NETPLAY_CTL_ALLOW_TIMESKIP, NULL)) + check2 = false; +#endif + if (check2) { if (input_st->flags & INP_FLAG_NONBLOCKING) @@ -6408,6 +6430,14 @@ static enum runloop_state_enum runloop_check_state( runloop_st->flags &= ~RUNLOOP_FLAG_SLOWMOTION; } +#ifdef HAVE_NETWORKING + if ((runloop_st->flags & RUNLOOP_FLAG_SLOWMOTION) + && !netplay_driver_ctl(RARCH_NETPLAY_CTL_ALLOW_TIMESKIP, NULL)) + { + runloop_st->flags &= ~RUNLOOP_FLAG_SLOWMOTION; + } +#endif + if (runloop_st->flags & RUNLOOP_FLAG_SLOWMOTION) { if (settings->uints.video_black_frame_insertion) @@ -7404,6 +7434,9 @@ bool core_set_netplay_callbacks(void) { runloop_state_t *runloop_st = &runloop_state; + if (netplay_driver_ctl(RARCH_NETPLAY_CTL_SKIP_NETPLAY_CALLBACKS, NULL)) + return true; + /* Force normal poll type for netplay. */ runloop_st->current_core.poll_type = POLL_TYPE_NORMAL; @@ -7983,7 +8016,8 @@ void runloop_path_set_redirect(settings_t *settings, #ifdef HAVE_NETWORKING /* Special save directory for netplay clients. */ if ( netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_ENABLED, NULL) - && !netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_SERVER, NULL)) + && !netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_SERVER, NULL) + && !netplay_driver_ctl(RARCH_NETPLAY_CTL_SKIP_NETPLAY_CALLBACKS, NULL)) { fill_pathname_join(new_savefile_dir, new_savefile_dir, ".netplay", sizeof(new_savefile_dir)); diff --git a/state_manager.c b/state_manager.c index 238517cb43b..98ea01bbc50 100644 --- a/state_manager.c +++ b/state_manager.c @@ -748,8 +748,9 @@ bool state_manager_check_rewind( { #ifdef HAVE_NETWORKING /* Make sure netplay isn't confused */ - if (!was_reversed) - netplay_driver_ctl(RARCH_NETPLAY_CTL_DESYNC_PUSH, NULL); + if (!was_reversed + && !netplay_driver_ctl(RARCH_NETPLAY_CTL_DESYNC_PUSH, NULL)) + return false; #endif rewind_st->flags |= STATE_MGR_REWIND_ST_FLAG_FRAME_IS_REVERSED; diff --git a/tasks/task_netplay_find_content.c b/tasks/task_netplay_find_content.c index 0dc84b94d7b..08cfee0f153 100644 --- a/tasks/task_netplay_find_content.c +++ b/tasks/task_netplay_find_content.c @@ -894,9 +894,7 @@ bool task_push_netplay_content_reload(const char *hostname) strlcpy(data->hostname, hostname, sizeof(data->hostname)); flags = content_get_flags(); - if (flags & CONTENT_ST_FLAG_CORE_DOES_NOT_NEED_CONTENT) - scan_state.state |= STATE_LOAD_CONTENTLESS; - else if (flags & CONTENT_ST_FLAG_IS_INITED) + if (flags & CONTENT_ST_FLAG_IS_INITED) { const char *psubsystem = path_get(RARCH_PATH_SUBSYSTEM); @@ -929,6 +927,10 @@ bool task_push_netplay_content_reload(const char *hostname) } } + if ((flags & CONTENT_ST_FLAG_CORE_DOES_NOT_NEED_CONTENT) && + !(scan_state.state & (STATE_LOAD|STATE_LOAD_SUBSYSTEM))) + scan_state.state |= STATE_LOAD_CONTENTLESS; + data->current.core_loaded = true; scan_state.running = true;