diff --git a/command.c b/command.c index e265254cdf..321b14e883 100644 --- a/command.c +++ b/command.c @@ -1919,11 +1919,6 @@ bool command_event(enum event_command cmd, void *data) if (bsv_movie_ctl(BSV_MOVIE_CTL_IS_INITED, NULL)) return false; -#ifdef HAVE_NETPLAY - if (netplay_driver_ctl(RARCH_NETPLAY_CTL_IS_DATA_INITED, NULL)) - return false; -#endif - #ifdef HAVE_CHEEVOS if (settings->cheevos.hardcore_mode_enable) return false; diff --git a/configuration.c b/configuration.c index 99c3504982..d120cc0935 100644 --- a/configuration.c +++ b/configuration.c @@ -911,7 +911,8 @@ static int populate_settings_int(settings_t *settings, struct config_int_setting SETTING_INT("state_slot", (unsigned*)&settings->state_slot, false, 0 /* TODO */, false); #ifdef HAVE_NETPLAY SETTING_INT("netplay_ip_port", &global->netplay.port, false, 0 /* TODO */, false); - SETTING_INT("netplay_delay_frames", &global->netplay.sync_frames, false, 0 /* TODO */, false); + SETTING_INT("netplay_delay_frames", &global->netplay.sync_frames, false, 16, false); + SETTING_INT("netplay_check_frames", &global->netplay.check_frames, false, 0, false); #endif #ifdef HAVE_LANGEXTRA SETTING_INT("user_language", &settings->user_language, true, RETRO_LANGUAGE_ENGLISH, false); @@ -1788,6 +1789,8 @@ static bool config_load_file(const char *path, bool set_defaults, #ifdef HAVE_NETPLAY if (!retroarch_override_setting_is_set(RARCH_OVERRIDE_SETTING_NETPLAY_DELAY_FRAMES)) CONFIG_GET_INT_BASE(conf, global, netplay.sync_frames, "netplay_delay_frames"); + if (!retroarch_override_setting_is_set(RARCH_OVERRIDE_SETTING_NETPLAY_CHECK_FRAMES)) + CONFIG_GET_INT_BASE(conf, global, netplay.sync_frames, "netplay_check_frames"); if (!retroarch_override_setting_is_set(RARCH_OVERRIDE_SETTING_NETPLAY_IP_PORT)) CONFIG_GET_INT_BASE(conf, global, netplay.port, "netplay_ip_port"); #endif diff --git a/core_impl.c b/core_impl.c index 1a5a9a667a..2175ca0dab 100644 --- a/core_impl.c +++ b/core_impl.c @@ -109,20 +109,10 @@ static bool core_init_libretro_cbs(void *data) /* Force normal poll type for netplay. */ core_poll_type = POLL_TYPE_NORMAL; - if (global->netplay.is_spectate) - { - core.retro_set_input_state( - (global->netplay.is_client ? - input_state_spectate_client : input_state_spectate) - ); - } - else - { - core.retro_set_video_refresh(video_frame_net); - core.retro_set_audio_sample(audio_sample_net); - core.retro_set_audio_sample_batch(audio_sample_batch_net); - core.retro_set_input_state(input_state_net); - } + core.retro_set_video_refresh(video_frame_net); + core.retro_set_audio_sample(audio_sample_net); + core.retro_set_audio_sample_batch(audio_sample_batch_net); + core.retro_set_input_state(input_state_net); #endif return true; diff --git a/intl/msg_hash_us.c b/intl/msg_hash_us.c index 36f0f7d8d4..dfefcb4f0b 100644 --- a/intl/msg_hash_us.c +++ b/intl/msg_hash_us.c @@ -1558,6 +1558,22 @@ int menu_hash_get_help_us_enum(enum msg_hash_enums msg, char *s, size_t len) "Increasing this value will increase \n" "performance, but introduce more latency."); break; + case MENU_ENUM_LABEL_NETPLAY_CHECK_FRAMES: + snprintf(s, len, + "The frequency in frames with which netplay \n" + "will verify that the host and client are in \n" + "sync. \n" + " \n" + "With most cores, this value will have no \n" + "visible effect and can be ignored. With \n" + "nondeterminstic cores, this value determines \n" + "how often the netplay peers will be brought \n" + "into sync. With buggy cores, setting this \n" + "to any non-zero value will cause severe \n" + "performance issues. Set to zero to perform \n" + "no checks. This value is only used on the \n" + "netplay host. \n"); + break; case MENU_ENUM_LABEL_VIDEO_MAX_SWAPCHAIN_IMAGES: snprintf(s, len, "Maximum amount of swapchain images. This \n" @@ -2424,6 +2440,8 @@ static const char *menu_hash_to_str_us_label_enum(enum msg_hash_enums msg) return "bluetooth_enable"; case MENU_ENUM_LABEL_NETPLAY_DELAY_FRAMES: return "netplay_delay_frames"; + case MENU_ENUM_LABEL_NETPLAY_CHECK_FRAMES: + return "netplay_check_frames"; case MENU_ENUM_LABEL_NETPLAY_MODE: return "netplay_mode"; case MENU_ENUM_LABEL_RGUI_SHOW_START_SCREEN: @@ -3747,6 +3765,8 @@ const char *msg_hash_to_str_us(enum msg_hash_enums msg) return "Bluetooth Enable"; case MENU_ENUM_LABEL_VALUE_NETPLAY_DELAY_FRAMES: return "Netplay Delay Frames"; + case MENU_ENUM_LABEL_VALUE_NETPLAY_CHECK_FRAMES: + return "Netplay Check Frames"; case MENU_ENUM_LABEL_VALUE_NETPLAY_MODE: return "Netplay Client Enable"; case MENU_ENUM_LABEL_VALUE_RGUI_SHOW_START_SCREEN: diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 3c880a3830..e01b84d658 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -4684,6 +4684,10 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type, void *data) MENU_ENUM_LABEL_NETPLAY_DELAY_FRAMES, PARSE_ONLY_UINT, false) != -1) count++; + if (menu_displaylist_parse_settings_enum(menu, info, + MENU_ENUM_LABEL_NETPLAY_CHECK_FRAMES, + PARSE_ONLY_UINT, false) != -1) + count++; if (menu_displaylist_parse_settings_enum(menu, info, MENU_ENUM_LABEL_NETPLAY_TCP_UDP_PORT, PARSE_ONLY_UINT, false) != -1) diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 930e1ec756..4bbc769230 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -1675,6 +1675,17 @@ void general_write_handler(void *data) } #endif break; + case MENU_ENUM_LABEL_NETPLAY_CHECK_FRAMES: +#ifdef HAVE_NETPLAY + { + bool val = (global->netplay.check_frames > 0); + + if (val) + retroarch_override_setting_set(RARCH_OVERRIDE_SETTING_NETPLAY_CHECK_FRAMES); + else + retroarch_override_setting_unset(RARCH_OVERRIDE_SETTING_NETPLAY_CHECK_FRAMES); + } +#endif default: break; } @@ -5814,6 +5825,21 @@ static bool setting_append_list( settings_data_list_current_add_flags(list, list_info, SD_FLAG_ADVANCED); menu_settings_list_current_add_enum_idx(list, list_info, MENU_ENUM_LABEL_NETPLAY_DELAY_FRAMES); + CONFIG_UINT( + list, list_info, + &global->netplay.check_frames, + msg_hash_to_str(MENU_ENUM_LABEL_NETPLAY_CHECK_FRAMES), + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NETPLAY_CHECK_FRAMES), + 0, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler); + menu_settings_list_current_add_range(list, list_info, 0, 10, 1, true, false); + settings_data_list_current_add_flags(list, list_info, SD_FLAG_ADVANCED); + menu_settings_list_current_add_enum_idx(list, list_info, MENU_ENUM_LABEL_NETPLAY_CHECK_FRAMES); + CONFIG_UINT( list, list_info, &global->netplay.port, diff --git a/msg_hash.h b/msg_hash.h index 2fcdbc3425..83b8eea021 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -1111,6 +1111,8 @@ enum msg_hash_enums MENU_ENUM_LABEL_VALUE_NETPLAY_CLIENT_SWAP_INPUT, MENU_ENUM_LABEL_NETPLAY_DELAY_FRAMES, MENU_ENUM_LABEL_VALUE_NETPLAY_DELAY_FRAMES, + MENU_ENUM_LABEL_NETPLAY_CHECK_FRAMES, + MENU_ENUM_LABEL_VALUE_NETPLAY_CHECK_FRAMES, MENU_ENUM_LABEL_NETPLAY_SPECTATOR_MODE_ENABLE, MENU_ENUM_LABEL_VALUE_NETPLAY_SPECTATOR_MODE_ENABLE, MENU_ENUM_LABEL_NETPLAY_TCP_UDP_PORT, diff --git a/network/netplay/README b/network/netplay/README index 5977cf63ab..8f0ca71057 100644 --- a/network/netplay/README +++ b/network/netplay/README @@ -41,15 +41,15 @@ ring, other is the first frame that it's unsafe to overwrite. Read is where it's read up to, which can be slightly ahead of other since it can't always immediately act upon new data. -In general, other ≤ read and other ≤ self. In all likelyhood, read ≤ self, but +In general, other ≤ read and other ≤ self. In all likelihood, read ≤ self, but it is both possible and supported for the remote host to get ahead of the local host. Pre-frame, Netplay serializes the core's state, polls for local input, and polls for input from the other side. If the input from the other side is too far behind, it stalls to allow the other side to catch up. To assure that this -stalling does not block the UI thread, it is implemented by rewinding the -thread every frame until data is ready. +stalling does not block the UI thread, it is implemented similarly to pausing, +rather than by blocking on the socket. If input has not been received for the other side up to the current frame (the usual case), the remote input is simulated in a simplistic manner. Each @@ -57,12 +57,13 @@ frame's local serialized state and simulated or real input goes into the frame buffers. During the frame of execution, when the core requests input, it receives the -input from the thread buffer, both local and real or simulated remote. +input from the state buffer, both local and real or simulated remote. Post-frame, it checks whether it's read more than it's actioned, i.e. if read > -other. If so, it rewinds to other (by loading the serialized state there) and -runs the core in replay mode with the real data up to read, then sets other = -read. +other self > other. If so, it first checks whether its simulated remote data +was correct. If it was, it simply moves other up. If not, it rewinds to other +(by loading the serialized state there) and runs the core in replay mode with +the real data up to the least of self and read, then sets other to that. When in Netplay mode, the callback for receiving input is replaced by input_state_net. It is the role of input_state_net to combine the true local @@ -79,3 +80,74 @@ which we've actually read self_frame_count+1 frames of local input. * Guarantee not actually a guarantee. + + +Netplay's command format + +Netplay commands consist of a 32-bit command identifier, followed by a 32-bit +payload size, both in network byte order, followed by a payload. The command +identifiers are listed in netplay.h. The commands are described below. Unless +specified otherwise, all payload values are in network byte order. + +Command: ACK +Payload: None +Description: + Acknowledgement. Not used. + +Command: NAK +Payload: None +Description: + Negative Acknowledgement. If received, the connection is terminated. Sent + whenever a command is malformed or otherwise not understood. + +Command: INPUT +Payload: + { + frame number: uint32 + joypad input: uint32 + analog 1 input: uint32 + analog 2 input: uint32 + OPTIONAL state CRC: uint32 + } +Description: + Input state for each frame. Netplay must send an INPUT command for every + frame in order to function at all. + +Command: FLIP_PLAYERS +Payload: + { + frame number: uint32 + } +Description: + Flip players at the requested frame. + +Command: DISCONNECT +Payload: None +Description: + Gracefully disconnect. Not used. + +Command: CRC +Payload: + { + frame number: uint32 + hash: uint32 + } +Description: + Informs the peer of the correct CRC hash for the specified frame. If the + receiver's hash doesn't match, they should send a REQUEST_SAVESTATE + command. + +Command: REQUEST_SAVESTATE +Payload: None +Description: + Requests that the peer send a savestate. + +Command: LOAD_SAVESTATE +Payload: + { + frame number: uint32 + serialized save state: blob (variable size) + } +Description: + Cause the other side to load a savestate, notionally one which the sending + side has also loaded. diff --git a/network/netplay/netplay.c b/network/netplay/netplay.c index 1d09b6b18d..8ae9e7a11d 100644 --- a/network/netplay/netplay.c +++ b/network/netplay/netplay.c @@ -157,11 +157,14 @@ static bool get_self_input_state(netplay_t *netplay) netplay->packet_buffer[4] = htonl(state[1]); netplay->packet_buffer[5] = htonl(state[2]); - if (!socket_send_all_blocking(netplay->fd, netplay->packet_buffer, sizeof(netplay->packet_buffer), false)) + if (!netplay->spectate.enabled) /* Spectate sends in its own way */ { - warn_hangup(); - netplay->has_connection = false; - return false; + if (!socket_send_all_blocking(netplay->fd, netplay->packet_buffer, sizeof(netplay->packet_buffer), false)) + { + warn_hangup(); + netplay->has_connection = false; + return false; + } } memcpy(ptr->self_state, state, sizeof(state)); @@ -192,6 +195,22 @@ static bool netplay_cmd_nak(netplay_t *netplay) return netplay_send_raw_cmd(netplay, NETPLAY_CMD_NAK, NULL, 0); } +bool netplay_cmd_crc(netplay_t *netplay, struct delta_frame *delta) +{ + uint32_t payload[2]; + payload[0] = htonl(delta->frame); + payload[1] = htonl(delta->crc); + return netplay_send_raw_cmd(netplay, NETPLAY_CMD_CRC, payload, sizeof(payload)); +} + +bool netplay_cmd_request_savestate(netplay_t *netplay) +{ + if (netplay->savestate_request_outstanding) + return true; + netplay->savestate_request_outstanding = true; + return netplay_send_raw_cmd(netplay, NETPLAY_CMD_REQUEST_SAVESTATE, NULL, 0); +} + static bool netplay_get_cmd(netplay_t *netplay) { uint32_t cmd; @@ -242,7 +261,12 @@ static bool netplay_get_cmd(netplay_t *netplay) for (i = 0; i < WORDS_PER_FRAME; i++) buffer[i] = ntohl(buffer[i]); - if (buffer[0] != netplay->read_frame_count) + if (buffer[0] < netplay->read_frame_count) + { + /* We already had this, so ignore the new transmission */ + return true; + } + else if (buffer[0] > netplay->read_frame_count) { /* Out of order = out of luck */ return netplay_cmd_nak(netplay); @@ -272,7 +296,7 @@ static bool netplay_get_cmd(netplay_t *netplay) flip_frame = ntohl(flip_frame); - if (flip_frame < netplay->flip_frame) + if (flip_frame < netplay->read_frame_count) { RARCH_ERR("Host asked us to flip users in the past. Not possible ...\n"); return netplay_cmd_nak(netplay); @@ -281,6 +305,12 @@ static bool netplay_get_cmd(netplay_t *netplay) netplay->flip ^= true; netplay->flip_frame = flip_frame; + /* Force a rewind to assure the flip happens: This just prevents us + * from skipping other past the flip because our prediction was + * correct */ + if (flip_frame < netplay->self_frame_count) + netplay->force_rewind = true; + RARCH_LOG("Netplay users are flipped.\n"); runloop_msg_queue_push("Netplay users are flipped.", 1, 180, false); @@ -294,16 +324,138 @@ static bool netplay_get_cmd(netplay_t *netplay) warn_hangup(); return true; + case NETPLAY_CMD_CRC: + { + uint32_t buffer[2]; + size_t tmp_ptr = netplay->self_ptr; + bool found = false; + + if (cmd_size != sizeof(buffer)) + { + RARCH_ERR("NETPLAY_CMD_CRC received unexpected payload size.\n"); + return netplay_cmd_nak(netplay); + } + + if (!socket_receive_all_blocking(netplay->fd, buffer, sizeof(buffer))) + { + RARCH_ERR("NETPLAY_CMD_CRC failed to receive payload.\n"); + return netplay_cmd_nak(netplay); + } + + buffer[0] = ntohl(buffer[0]); + buffer[1] = ntohl(buffer[1]); + + /* Received a CRC for some frame. If we still have it, check if it + * matched. This approach could be improved with some quick modular + * arithmetic. */ + do { + if (netplay->buffer[tmp_ptr].frame == buffer[0]) + { + found = true; + break; + } + + tmp_ptr = PREV_PTR(tmp_ptr); + } while (tmp_ptr != netplay->self_ptr); + + if (!found) + { + /* Oh well, we got rid of it! */ + return true; + } + + if (buffer[0] <= netplay->other_frame_count) + { + /* We've already replayed up to this frame, so we can check it + * directly */ + uint32_t local_crc = netplay_delta_frame_crc(netplay, &netplay->buffer[tmp_ptr]); + if (buffer[1] != local_crc) + { + /* Problem! */ + netplay_cmd_request_savestate(netplay); + } + } + else + { + /* We'll have to check it when we catch up */ + netplay->buffer[tmp_ptr].crc = buffer[1]; + } + + return true; + } + + case NETPLAY_CMD_REQUEST_SAVESTATE: + /* Delay until next frame so we don't send the savestate after the + * input */ + netplay->force_send_savestate = true; + return true; + case NETPLAY_CMD_LOAD_SAVESTATE: - RARCH_ERR("NETPLAY_CMD_LOAD_SAVESTATE unimplemented.\n"); - return netplay_cmd_nak(netplay); + { + uint32_t frame; + + /* There is a subtlty in whether the load comes before or after the + * current frame: + * + * If it comes before the current frame, then we need to force a + * rewind to that point. + * + * If it comes after the current frame, we need to jump ahead, then + * (strangely) force a rewind to the frame we're already on, so it + * gets loaded. This is just to avoid having reloading implemented in + * too many places. */ + if (cmd_size > netplay->state_size + sizeof(uint32_t)) + { + RARCH_ERR("CMD_LOAD_SAVESTATE received an unexpected save state size.\n"); + return netplay_cmd_nak(netplay); + } + + if (!socket_receive_all_blocking(netplay->fd, &frame, sizeof(frame))) + { + RARCH_ERR("CMD_LOAD_SAVESTATE failed to receive savestate frame.\n"); + return netplay_cmd_nak(netplay); + } + frame = ntohl(frame); + + if (frame != netplay->read_frame_count) + { + RARCH_ERR("CMD_LOAD_SAVESTATE loading a state out of order!\n"); + return netplay_cmd_nak(netplay); + } + + if (!socket_receive_all_blocking(netplay->fd, + netplay->buffer[netplay->read_ptr].state, cmd_size - sizeof(uint32_t))) + { + RARCH_ERR("CMD_LOAD_SAVESTATE failed to receive savestate.\n"); + return netplay_cmd_nak(netplay); + } + + /* Skip ahead if it's past where we are */ + if (frame > netplay->self_frame_count) + { + /* This is squirrely: We need to assure that when we advance the + * frame in post_frame, THEN we're referring to the frame to + * load into. If we refer directly to read_ptr, then we'll end + * up never reading the input for read_frame_count itself, which + * will make the other side unhappy. */ + netplay->self_ptr = PREV_PTR(netplay->read_ptr); + netplay->self_frame_count = frame - 1; + } + + /* And force rewind to it */ + netplay->force_rewind = true; + netplay->savestate_request_outstanding = false; + netplay->other_ptr = netplay->read_ptr; + netplay->other_frame_count = frame; + return true; + } case NETPLAY_CMD_PAUSE: - command_event(CMD_EVENT_PAUSE, NULL); + netplay->remote_paused = true; return true; case NETPLAY_CMD_RESUME: - command_event(CMD_EVENT_UNPAUSE, NULL); + netplay->remote_paused = false; return true; default: break; @@ -360,7 +512,7 @@ static int poll_input(netplay_t *netplay, bool block) RARCH_LOG("Network is stalling at frame %u, count %u of %d ...\n", netplay->self_frame_count, netplay->timeout_cnt, MAX_RETRIES); - if (netplay->timeout_cnt >= MAX_RETRIES) + if (netplay->timeout_cnt >= MAX_RETRIES && !netplay->remote_paused) return -1; } while (had_input || (block && (netplay->read_frame_count <= netplay->self_frame_count))); @@ -401,6 +553,10 @@ static bool netplay_poll(netplay_t *netplay) get_self_input_state(netplay); + /* No network side in spectate mode */ + if (netplay_is_server(netplay) && netplay->spectate.enabled) + return true; + /* Read Netplay input, block if we're configured to stall for input every * frame */ res = poll_input(netplay, (netplay->stall_frames == 0) && (netplay->read_frame_count <= netplay->self_frame_count)); @@ -434,7 +590,12 @@ static bool netplay_poll(netplay_t *netplay) if (netplay->stall) { retro_time_t now = cpu_features_get_time_usec(); - if (now - netplay->stall_time >= MAX_STALL_TIME_USEC) + if (netplay->remote_paused) + { + /* Don't stall out while they're paused */ + netplay->stall_time = now; + } + else if (now - netplay->stall_time >= MAX_STALL_TIME_USEC) { /* Stalled out! */ netplay->has_connection = false; @@ -745,11 +906,47 @@ static bool init_socket(netplay_t *netplay, const char *server, uint16_t port) return true; } +static bool netplay_init_buffers(netplay_t *netplay, unsigned frames) +{ + unsigned i; + retro_ctx_size_info_t info; + + if (!netplay) + return false; + + /* * 2 + 1 because: + * Self sits in the middle, + * Other is allowed to drift as much as 'frames' frames behind + * Read is allowed to drift as much as 'frames' frames ahead */ + netplay->buffer_size = frames * 2 + 1; + + netplay->buffer = (struct delta_frame*)calloc(netplay->buffer_size, + sizeof(*netplay->buffer)); + + if (!netplay->buffer) + return false; + + core_serialize_size(&info); + + netplay->state_size = info.size; + + for (i = 0; i < netplay->buffer_size; i++) + { + netplay->buffer[i].state = calloc(netplay->state_size, 1); + + if (!netplay->buffer[i].state) + return false; + } + + return true; +} + /** * netplay_new: * @server : IP address of server. * @port : Port of server. * @frames : Amount of lag frames. + * @check_frames : Frequency with which to check CRCs. * @cb : Libretro callbacks. * @spectate : If true, enable spectator mode. * @nick : Nickname of user. @@ -760,9 +957,8 @@ static bool init_socket(netplay_t *netplay, const char *server, uint16_t port) * Returns: new netplay handle. **/ netplay_t *netplay_new(const char *server, uint16_t port, - unsigned frames, const struct retro_callbacks *cb, - bool spectate, - const char *nick) + unsigned frames, unsigned check_frames, const struct retro_callbacks *cb, + bool spectate, const char *nick) { netplay_t *netplay = (netplay_t*)calloc(1, sizeof(*netplay)); if (!netplay) @@ -775,6 +971,13 @@ netplay_t *netplay_new(const char *server, uint16_t port, netplay->is_server = server == NULL; strlcpy(netplay->nick, nick, sizeof(netplay->nick)); netplay->stall_frames = frames; + netplay->check_frames = check_frames; + + if (!netplay_init_buffers(netplay, frames)) + { + free(netplay); + return NULL; + } if(spectate) netplay->net_cbs = netplay_get_cbs_spectate(); @@ -859,7 +1062,8 @@ error: **/ static void netplay_flip_users(netplay_t *netplay) { - uint32_t flip_frame = netplay->self_frame_count + 32; /* FIXME: This value is now arbitrary */ + /* Must be in the future because we may have already sent this frame's data */ + uint32_t flip_frame = netplay->self_frame_count + 1; uint32_t flip_frame_net = htonl(flip_frame); bool command = netplay_command( netplay, NETPLAY_CMD_FLIP_PLAYERS, @@ -908,67 +1112,27 @@ void netplay_free(netplay_t *netplay) free(netplay); } - -static void netplay_set_spectate_input(netplay_t *netplay, int16_t input) -{ - if (netplay->spectate.input_ptr >= netplay->spectate.input_sz) - { - netplay->spectate.input_sz++; - netplay->spectate.input_sz *= 2; - netplay->spectate.input = (uint16_t*)realloc(netplay->spectate.input, - netplay->spectate.input_sz * sizeof(uint16_t)); - } - - netplay->spectate.input[netplay->spectate.input_ptr++] = swap_if_big16(input); -} - -int16_t input_state_spectate(unsigned port, unsigned device, - unsigned idx, unsigned id) -{ - netplay_t *netplay = (netplay_t*)netplay_data; - int16_t res = netplay->cbs.state_cb(port, device, idx, id); - - netplay_set_spectate_input(netplay, res); - return res; -} - -static int16_t netplay_get_spectate_input(netplay_t *netplay, bool port, - unsigned device, unsigned idx, unsigned id) -{ - int16_t inp; - retro_ctx_input_state_info_t input_info; - - if (socket_receive_all_blocking(netplay->fd, (char*)&inp, sizeof(inp))) - return swap_if_big16(inp); - - RARCH_ERR("Connection with host was cut.\n"); - runloop_msg_queue_push("Connection with host was cut.", 1, 180, true); - - input_info.cb = netplay->cbs.state_cb; - - core_set_input_state(&input_info); - - return netplay->cbs.state_cb(port, device, idx, id); -} - -int16_t input_state_spectate_client(unsigned port, unsigned device, - unsigned idx, unsigned id) -{ - return netplay_get_spectate_input((netplay_t*)netplay_data, port, - device, idx, id); -} - /** * netplay_pre_frame: * @netplay : pointer to netplay object * * Pre-frame for Netplay. * Call this before running retro_run(). + * + * Returns: true (1) if the frontend is cleared to emulate the frame, false (0) + * if we're stalled or paused **/ -void netplay_pre_frame(netplay_t *netplay) +bool netplay_pre_frame(netplay_t *netplay) { retro_assert(netplay && netplay->net_cbs->pre_frame); - netplay->net_cbs->pre_frame(netplay); + if (netplay->local_paused) + { + /* FIXME: This is an ugly way to learn we're not paused anymore */ + netplay_frontend_paused(netplay, false); + } + if (!netplay->net_cbs->pre_frame(netplay)) + return false; + return (!netplay->has_connection || (!netplay->stall && !netplay->remote_paused)); } /** @@ -985,6 +1149,80 @@ void netplay_post_frame(netplay_t *netplay) netplay->net_cbs->post_frame(netplay); } +/** + * netplay_frontend_paused + * @netplay : pointer to netplay object + * @paused : true if frontend is paused + * + * Inform Netplay of the frontend's pause state (paused or otherwise) + **/ +void netplay_frontend_paused(netplay_t *netplay, bool paused) +{ + /* Nothing to do if we already knew this */ + if (netplay->local_paused == paused) + return; + + netplay->local_paused = paused; + if (netplay->has_connection && !netplay->spectate.enabled) + netplay_send_raw_cmd(netplay, paused ? NETPLAY_CMD_PAUSE : NETPLAY_CMD_RESUME, NULL, 0); +} + +/** + * netplay_load_savestate + * @netplay : pointer to netplay object + * @serial_info : the savestate being loaded + * @save : whether to save the provided serial_info into the frame buffer + * + * Inform Netplay of a savestate load and send it to the other side + **/ +void netplay_load_savestate(netplay_t *netplay, retro_ctx_serialize_info_t *serial_info, bool save) +{ + uint32_t header[3]; + + if (!netplay->has_connection) + return; + + /* Record it in our own buffer */ + if (save && netplay_delta_frame_ready(netplay, &netplay->buffer[netplay->self_ptr], netplay->self_frame_count)) + { + if (serial_info->size <= netplay->state_size) + { + memcpy(netplay->buffer[netplay->self_ptr].state, + serial_info->data_const, serial_info->size); + } + } + + /* We need to ignore any intervening data from the other side, and never rewind past this */ + if (netplay->read_frame_count < netplay->self_frame_count) + { + netplay->read_ptr = netplay->self_ptr; + netplay->read_frame_count = netplay->self_frame_count; + } + if (netplay->other_frame_count < netplay->self_frame_count) + { + netplay->other_ptr = netplay->self_ptr; + netplay->other_frame_count = netplay->self_frame_count; + } + + /* And send it to the peer (FIXME: this is an ugly way to do this) */ + header[0] = htonl(NETPLAY_CMD_LOAD_SAVESTATE); + header[1] = htonl(serial_info->size + sizeof(uint32_t)); + header[2] = htonl(netplay->self_frame_count); + if (!socket_send_all_blocking(netplay->fd, header, sizeof(header), false)) + { + warn_hangup(); + netplay->has_connection = false; + return; + } + + if (!socket_send_all_blocking(netplay->fd, serial_info->data_const, serial_info->size, false)) + { + warn_hangup(); + netplay->has_connection = false; + return; + } +} + void deinit_netplay(void) { netplay_t *netplay = (netplay_t*)netplay_data; @@ -1032,8 +1270,8 @@ bool init_netplay(void) netplay_data = (netplay_t*)netplay_new( global->netplay.is_client ? global->netplay.server : NULL, global->netplay.port ? global->netplay.port : RARCH_DEFAULT_PORT, - global->netplay.sync_frames, &cbs, global->netplay.is_spectate, - settings->username); + global->netplay.sync_frames, global->netplay.check_frames, &cbs, + global->netplay.is_spectate, settings->username); if (netplay_data) return true; @@ -1050,7 +1288,12 @@ bool init_netplay(void) bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data) { if (!netplay_data) - return false; + { + if (state == RARCH_NETPLAY_CTL_IS_DATA_INITED) + return false; + else + return true; + } switch (state) { @@ -1060,8 +1303,7 @@ bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data) netplay_post_frame((netplay_t*)netplay_data); break; case RARCH_NETPLAY_CTL_PRE_FRAME: - netplay_pre_frame((netplay_t*)netplay_data); - break; + return netplay_pre_frame((netplay_t*)netplay_data); case RARCH_NETPLAY_CTL_FLIP_PLAYERS: { bool *state = (bool*)data; @@ -1076,6 +1318,15 @@ bool netplay_driver_ctl(enum rarch_netplay_ctl_state state, void *data) command_event(CMD_EVENT_FULLSCREEN_TOGGLE, NULL); } break; + case RARCH_NETPLAY_CTL_PAUSE: + netplay_frontend_paused((netplay_t*)netplay_data, true); + break; + case RARCH_NETPLAY_CTL_UNPAUSE: + netplay_frontend_paused((netplay_t*)netplay_data, false); + break; + case RARCH_NETPLAY_CTL_LOAD_SAVESTATE: + netplay_load_savestate((netplay_t*)netplay_data, (retro_ctx_serialize_info_t*)data, true); + break; default: case RARCH_NETPLAY_CTL_NONE: break; diff --git a/network/netplay/netplay.h b/network/netplay/netplay.h index 3aac5bf1c0..ecac020d55 100644 --- a/network/netplay/netplay.h +++ b/network/netplay/netplay.h @@ -35,7 +35,10 @@ enum rarch_netplay_ctl_state RARCH_NETPLAY_CTL_FULLSCREEN_TOGGLE, RARCH_NETPLAY_CTL_POST_FRAME, RARCH_NETPLAY_CTL_PRE_FRAME, - RARCH_NETPLAY_CTL_IS_DATA_INITED + RARCH_NETPLAY_CTL_IS_DATA_INITED, + RARCH_NETPLAY_CTL_PAUSE, + RARCH_NETPLAY_CTL_UNPAUSE, + RARCH_NETPLAY_CTL_LOAD_SAVESTATE }; enum netplay_cmd @@ -73,8 +76,15 @@ enum netplay_cmd /* Loading and synchronization */ + /* Send the CRC hash of a frame's state */ + NETPLAY_CMD_CRC = 0x0010, + + /* Request a savestate */ + NETPLAY_CMD_REQUEST_SAVESTATE = 0x0011, + /* Send a savestate for the client to load */ NETPLAY_CMD_LOAD_SAVESTATE = 0x0012, + /* Sends over cheats enabled on client */ NETPLAY_CMD_CHEATS = 0x0013, @@ -115,17 +125,12 @@ void audio_sample_net(int16_t left, int16_t right); size_t audio_sample_batch_net(const int16_t *data, size_t frames); -int16_t input_state_spectate(unsigned port, unsigned device, - unsigned idx, unsigned id); - -int16_t input_state_spectate_client(unsigned port, unsigned device, - unsigned idx, unsigned id); - /** * netplay_new: * @server : IP address of server. * @port : Port of server. * @frames : Amount of lag frames. + * @check_frames : Frequency with which to check CRCs. * @cb : Libretro callbacks. * @spectate : If true, enable spectator mode. * @nick : Nickname of user. @@ -136,7 +141,7 @@ int16_t input_state_spectate_client(unsigned port, unsigned device, * Returns: new netplay handle. **/ netplay_t *netplay_new(const char *server, - uint16_t port, unsigned frames, + uint16_t port, unsigned frames, unsigned check_frames, const struct retro_callbacks *cb, bool spectate, const char *nick); @@ -154,8 +159,11 @@ void netplay_free(netplay_t *handle); * * Pre-frame for Netplay. * Call this before running retro_run(). + * + * Returns: true (1) if the frontend is clear to emulate the frame, false (0) + * if we're stalled or paused **/ -void netplay_pre_frame(netplay_t *handle); +bool netplay_pre_frame(netplay_t *handle); /** * netplay_post_frame: @@ -167,6 +175,25 @@ void netplay_pre_frame(netplay_t *handle); **/ void netplay_post_frame(netplay_t *handle); +/** + * netplay_frontend_paused + * @netplay : pointer to netplay object + * @paused : true if frontend is paused + * + * Inform Netplay of the frontend's pause state (paused or otherwise) + **/ +void netplay_frontend_paused(netplay_t *netplay, bool paused); + +/** + * netplay_load_savestate + * @netplay : pointer to netplay object + * @serial_info : the savestate being loaded + * @save : whether to save the provided serial_info into the frame buffer + * + * Inform Netplay of a savestate load and send it to the other side + **/ +void netplay_load_savestate(netplay_t *netplay, retro_ctx_serialize_info_t *serial_info, bool save); + /** * init_netplay: * diff --git a/network/netplay/netplay_common.c b/network/netplay/netplay_common.c index a1b26e1154..1f4b89fca2 100644 --- a/network/netplay/netplay_common.c +++ b/network/netplay/netplay_common.c @@ -20,6 +20,8 @@ #include "netplay_private.h" #include +#include "compat/zlib.h" + #include "../../movie.h" #include "../../msg_hash.h" #include "../../content.h" @@ -365,3 +367,10 @@ bool netplay_delta_frame_ready(netplay_t *netplay, struct delta_frame *delta, ui delta->state = remember_state; return true; } + +uint32_t netplay_delta_frame_crc(netplay_t *netplay, struct delta_frame *delta) +{ + if (!netplay->state_size) + return 0; + return crc32(0L, delta->state, netplay->state_size); +} diff --git a/network/netplay/netplay_net.c b/network/netplay/netplay_net.c index ee9a13c5df..56ede8892e 100644 --- a/network/netplay/netplay_net.c +++ b/network/netplay/netplay_net.c @@ -24,13 +24,35 @@ #include "../../autosave.h" +static void netplay_handle_frame_hash(netplay_t *netplay, struct delta_frame *delta) +{ + if (netplay_is_server(netplay)) + { + if (netplay->check_frames && delta->frame % netplay->check_frames == 0) + { + delta->crc = netplay_delta_frame_crc(netplay, delta); + netplay_cmd_crc(netplay, delta); + } + } + else if (delta->crc) + { + /* We have a remote CRC, so check it */ + uint32_t local_crc = netplay_delta_frame_crc(netplay, delta); + if (local_crc != delta->crc) + { + /* Fix this! */ + netplay_cmd_request_savestate(netplay); + } + } +} + /** - * pre_frame: + * netplay_net_pre_frame: * @netplay : pointer to netplay object * * Pre-frame for Netplay (normal version). **/ -static void netplay_net_pre_frame(netplay_t *netplay) +static bool netplay_net_pre_frame(netplay_t *netplay) { retro_ctx_serialize_info_t serial_info; @@ -40,7 +62,17 @@ static void netplay_net_pre_frame(netplay_t *netplay) serial_info.data = netplay->buffer[netplay->self_ptr].state; serial_info.size = netplay->state_size; - if (!core_serialize(&serial_info)) + if (core_serialize(&serial_info)) + { + if (netplay->force_send_savestate) + { + /* Send this along to the other side */ + serial_info.data_const = netplay->buffer[netplay->self_ptr].state; + netplay_load_savestate(netplay, &serial_info, false); + netplay->force_send_savestate = false; + } + } + else { /* If the core can't serialize properly, we must stall for the * remote input on EVERY frame, because we can't recover */ @@ -51,10 +83,12 @@ static void netplay_net_pre_frame(netplay_t *netplay) netplay->can_poll = true; input_poll_net(); + + return true; } /** - * post_frame: + * netplay_net_post_frame: * @netplay : pointer to netplay object * * Post-frame for Netplay (normal version). @@ -69,24 +103,29 @@ static void netplay_net_post_frame(netplay_t *netplay) if (!netplay->has_connection) return; - /* Skip ahead if we predicted correctly. - * Skip until our simulation failed. */ - while (netplay->other_frame_count < netplay->read_frame_count && - netplay->other_frame_count < netplay->self_frame_count) + if (!netplay->force_rewind) { - const struct delta_frame *ptr = &netplay->buffer[netplay->other_ptr]; + /* Skip ahead if we predicted correctly. + * Skip until our simulation failed. */ + while (netplay->other_frame_count < netplay->read_frame_count && + netplay->other_frame_count < netplay->self_frame_count) + { + struct delta_frame *ptr = &netplay->buffer[netplay->other_ptr]; - if (memcmp(ptr->simulated_input_state, ptr->real_input_state, - sizeof(ptr->real_input_state)) != 0 - && !ptr->used_real) - break; - netplay->other_ptr = NEXT_PTR(netplay->other_ptr); - netplay->other_frame_count++; + if (memcmp(ptr->simulated_input_state, ptr->real_input_state, + sizeof(ptr->real_input_state)) != 0 + && !ptr->used_real) + break; + netplay_handle_frame_hash(netplay, ptr); + netplay->other_ptr = NEXT_PTR(netplay->other_ptr); + netplay->other_frame_count++; + } } /* Now replay the real input if we've gotten ahead of it */ - if (netplay->other_frame_count < netplay->read_frame_count && - netplay->other_frame_count < netplay->self_frame_count) + if (netplay->force_rewind || + (netplay->other_frame_count < netplay->read_frame_count && + netplay->other_frame_count < netplay->self_frame_count)) { retro_ctx_serialize_info_t serial_info; @@ -95,23 +134,23 @@ static void netplay_net_post_frame(netplay_t *netplay) netplay->replay_ptr = netplay->other_ptr; netplay->replay_frame_count = netplay->other_frame_count; - if (netplay->replay_frame_count < netplay->self_frame_count) - { - serial_info.data = NULL; - serial_info.data_const = netplay->buffer[netplay->replay_ptr].state; - serial_info.size = netplay->state_size; + serial_info.data = NULL; + serial_info.data_const = netplay->buffer[netplay->replay_ptr].state; + serial_info.size = netplay->state_size; - core_unserialize(&serial_info); - } + core_unserialize(&serial_info); while (netplay->replay_frame_count < netplay->self_frame_count) { - serial_info.data = netplay->buffer[netplay->replay_ptr].state; + struct delta_frame *ptr = &netplay->buffer[netplay->replay_ptr]; + serial_info.data = ptr->state; serial_info.size = netplay->state_size; serial_info.data_const = NULL; core_serialize(&serial_info); + netplay_handle_frame_hash(netplay, ptr); + #if defined(HAVE_THREADS) autosave_lock(); #endif @@ -134,9 +173,11 @@ static void netplay_net_post_frame(netplay_t *netplay) netplay->other_frame_count = netplay->self_frame_count; } netplay->is_replay = false; + netplay->force_rewind = false; } - /* If we're supposed to stall, rewind */ + /* If we're supposed to stall, rewind (we shouldn't get this far if we're + * stalled, so this is a last resort) */ if (netplay->stall) { retro_ctx_serialize_info_t serial_info; @@ -151,34 +192,6 @@ static void netplay_net_post_frame(netplay_t *netplay) core_unserialize(&serial_info); } } -static bool netplay_net_init_buffers(netplay_t *netplay) -{ - unsigned i; - retro_ctx_size_info_t info; - - if (!netplay) - return false; - - netplay->buffer = (struct delta_frame*)calloc(netplay->buffer_size, - sizeof(*netplay->buffer)); - - if (!netplay->buffer) - return false; - - core_serialize_size(&info); - - netplay->state_size = info.size; - - for (i = 0; i < netplay->buffer_size; i++) - { - netplay->buffer[i].state = malloc(netplay->state_size); - - if (!netplay->buffer[i].state) - return false; - } - - return true; -} static bool netplay_net_info_cb(netplay_t* netplay, unsigned frames) { @@ -193,15 +206,6 @@ static bool netplay_net_info_cb(netplay_t* netplay, unsigned frames) return false; } - /* * 2 + 1 because: - * Self sits in the middle, - * Other is allowed to drift as much as 'frames' frames behind - * Read is allowed to drift as much as 'frames' frames ahead */ - netplay->buffer_size = frames * 2 + 1; - - if (!netplay_net_init_buffers(netplay)) - return false; - netplay->has_connection = true; return true; diff --git a/network/netplay/netplay_private.h b/network/netplay/netplay_private.h index e8bf820e40..49fe4fd280 100644 --- a/network/netplay/netplay_private.h +++ b/network/netplay/netplay_private.h @@ -45,8 +45,12 @@ struct delta_frame bool used; /* a bit derpy, but this is how we know if the delta's been used at all */ uint32_t frame; + /* The serialized state of the core at this frame, before input */ void *state; + /* The CRC-32 of the serialized state if we've calculated it, else 0 */ + uint32_t crc; + uint32_t real_input_state[WORDS_PER_FRAME - 1]; uint32_t simulated_input_state[WORDS_PER_FRAME - 1]; uint32_t self_state[WORDS_PER_FRAME - 1]; @@ -62,7 +66,7 @@ struct delta_frame }; struct netplay_callbacks { - void (*pre_frame) (netplay_t *netplay); + bool (*pre_frame) (netplay_t *netplay); void (*post_frame)(netplay_t *netplay); bool (*info_cb) (netplay_t *netplay, unsigned frames); }; @@ -103,10 +107,20 @@ struct netplay /* Are we replaying old frames? */ bool is_replay; + /* We don't want to poll several times on a frame. */ bool can_poll; - /* If we end up having to drop remote frame data because it's ahead of us, fast-forward is URGENT */ - bool must_fast_forward; + + /* Force a rewind to other_frame_count/other_ptr. This is for synchronized + * events, such as player flipping or savestate loading. */ + bool force_rewind; + + /* Force our state to be sent to the other side. Used when they request a + * savestate, to send at the next pre-frame. */ + bool force_send_savestate; + + /* Have we requested a savestate as a sync point? */ + bool savestate_request_outstanding; /* A buffer for outgoing input packets. */ uint32_t packet_buffer[2 + WORDS_PER_FRAME]; @@ -124,29 +138,32 @@ struct netplay struct { bool enabled; int fds[MAX_SPECTATORS]; + uint32_t frames[MAX_SPECTATORS]; uint16_t *input; size_t input_ptr; size_t input_sz; } spectate; bool is_server; + /* User flipping - * Flipping state. If ptr >= flip_frame, we apply the flip. - * If not, we apply the opposite, effectively creating a trigger point. - * To avoid collition we need to make sure our client/host is synced up - * well after flip_frame before allowing another flip. */ + * Flipping state. If frame >= flip_frame, we apply the flip. + * If not, we apply the opposite, effectively creating a trigger point. */ bool flip; uint32_t flip_frame; /* Netplay pausing */ - bool pause; - uint32_t pause_frame; + bool local_paused; + bool remote_paused; /* And stalling */ uint32_t stall_frames; int stall; retro_time_t stall_time; + /* Frequency with which to check CRCs */ + uint32_t check_frames; + struct netplay_callbacks* net_cbs; }; @@ -181,4 +198,10 @@ bool netplay_is_spectate(netplay_t* netplay); bool netplay_delta_frame_ready(netplay_t *netplay, struct delta_frame *delta, uint32_t frame); +uint32_t netplay_delta_frame_crc(netplay_t *netplay, struct delta_frame *delta); + +bool netplay_cmd_crc(netplay_t *netplay, struct delta_frame *delta); + +bool netplay_cmd_request_savestate(netplay_t *netplay); + #endif diff --git a/network/netplay/netplay_spectate.c b/network/netplay/netplay_spectate.c index c122b20b88..e6dbbd8aac 100644 --- a/network/netplay/netplay_spectate.c +++ b/network/netplay/netplay_spectate.c @@ -1,6 +1,7 @@ /* RetroArch - A frontend for libretro. * Copyright (C) 2010-2014 - Hans-Kristian Arntzen * Copyright (C) 2011-2016 - Daniel De Matteis + * Copyright (C) 2016 - Gregor Richards * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- @@ -14,10 +15,8 @@ * If not, see . */ +#include #include -#include -#include -#include #include #include @@ -25,157 +24,266 @@ #include "netplay_private.h" -#include "../../runloop.h" +#include "retro_assert.h" + +#include "../../autosave.h" /** - * netplay_pre_frame_spectate: + * netplay_spectate_pre_frame: * @netplay : pointer to netplay object * - * Pre-frame for Netplay (spectate mode version). + * Pre-frame for Netplay (spectator version). **/ -static void netplay_spectate_pre_frame(netplay_t *netplay) +static bool netplay_spectate_pre_frame(netplay_t *netplay) { - unsigned i; - uint32_t *header; - int new_fd, idx, bufsize; - size_t header_size; - struct sockaddr_storage their_addr; - socklen_t addr_size; - fd_set fds; - struct timeval tmp_tv = {0}; - - if (!netplay_is_server(netplay)) - return; - - FD_ZERO(&fds); - FD_SET(netplay->fd, &fds); - - if (socket_select(netplay->fd + 1, &fds, NULL, NULL, &tmp_tv) <= 0) - return; - - if (!FD_ISSET(netplay->fd, &fds)) - return; - - addr_size = sizeof(their_addr); - new_fd = accept(netplay->fd, (struct sockaddr*)&their_addr, &addr_size); - if (new_fd < 0) + if (netplay_is_server(netplay)) { - RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_ACCEPT_INCOMING_SPECTATOR)); - return; - } + fd_set fds; + struct timeval tmp_tv = {0}; + int new_fd, idx, i; + struct sockaddr_storage their_addr; + socklen_t addr_size; + retro_ctx_serialize_info_t serial_info; + uint32_t header[3]; - idx = -1; - for (i = 0; i < MAX_SPECTATORS; i++) - { - if (netplay->spectate.fds[i] == -1) + netplay->can_poll = true; + input_poll_net(); + + /* Send our input to any connected spectators */ + for (i = 0; i < MAX_SPECTATORS; i++) { - idx = i; - break; + if (netplay->spectate.fds[i] >= 0) + { + netplay->packet_buffer[2] = htonl(netplay->self_frame_count - netplay->spectate.frames[i]); + if (!socket_send_all_blocking(netplay->spectate.fds[i], netplay->packet_buffer, sizeof(netplay->packet_buffer), false)) + { + socket_close(netplay->spectate.fds[i]); + netplay->spectate.fds[i] = -1; + } + } } - } - /* No vacant client streams :( */ - if (idx == -1) + /* Check for connections */ + FD_ZERO(&fds); + FD_SET(netplay->fd, &fds); + if (socket_select(netplay->fd + 1, &fds, NULL, NULL, &tmp_tv) <= 0) + return true; + + if (!FD_ISSET(netplay->fd, &fds)) + return true; + + addr_size = sizeof(their_addr); + new_fd = accept(netplay->fd, (struct sockaddr*)&their_addr, &addr_size); + if (new_fd < 0) + { + RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_ACCEPT_INCOMING_SPECTATOR)); + return true; + } + + idx = -1; + for (i = 0; i < MAX_SPECTATORS; i++) + { + if (netplay->spectate.fds[i] == -1) + { + idx = i; + break; + } + } + + /* No vacant client streams :( */ + if (idx == -1) + { + socket_close(new_fd); + return true; + } + + if (!netplay_get_nickname(netplay, new_fd)) + { + RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_GET_NICKNAME_FROM_CLIENT)); + socket_close(new_fd); + return true; + } + + if (!netplay_send_nickname(netplay, new_fd)) + { + RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_SEND_NICKNAME_TO_CLIENT)); + socket_close(new_fd); + return true; + } + + /* Start them at the current frame */ + netplay->spectate.frames[idx] = netplay->self_frame_count; + serial_info.data_const = NULL; + serial_info.data = netplay->buffer[netplay->self_ptr].state; + serial_info.size = netplay->state_size; + if (core_serialize(&serial_info)) + { + /* Send them the savestate */ + header[0] = htonl(NETPLAY_CMD_LOAD_SAVESTATE); + header[1] = htonl(serial_info.size + sizeof(uint32_t)); + header[2] = htonl(0); + if (!socket_send_all_blocking(new_fd, header, sizeof(header), false)) + { + socket_close(new_fd); + return true; + } + + if (!socket_send_all_blocking(new_fd, serial_info.data, serial_info.size, false)) + { + socket_close(new_fd); + return true; + } + } + + /* And send them this frame's input */ + netplay->packet_buffer[2] = htonl(0); + if (!socket_send_all_blocking(new_fd, netplay->packet_buffer, sizeof(netplay->packet_buffer), false)) + { + socket_close(new_fd); + return true; + } + + netplay->spectate.fds[idx] = new_fd; + + } + else { - socket_close(new_fd); - return; + if (netplay_delta_frame_ready(netplay, &netplay->buffer[netplay->self_ptr], netplay->self_frame_count)) + { + /* Mark our own data as already read, so we ignore local input */ + netplay->buffer[netplay->self_ptr].have_local = true; + } + + netplay->can_poll = true; + input_poll_net(); + + /* Only proceed if we have data */ + if (netplay->read_frame_count <= netplay->self_frame_count) + return false; + } - if (!netplay_get_nickname(netplay, new_fd)) - { - RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_GET_NICKNAME_FROM_CLIENT)); - socket_close(new_fd); - return; - } - - if (!netplay_send_nickname(netplay, new_fd)) - { - RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_SEND_NICKNAME_TO_CLIENT)); - socket_close(new_fd); - return; - } - - header = netplay_bsv_header_generate(&header_size, - netplay_impl_magic()); - - if (!header) - { - RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_GENERATE_BSV_HEADER)); - socket_close(new_fd); - return; - } - - bufsize = header_size; - setsockopt(new_fd, SOL_SOCKET, SO_SNDBUF, (const char*)&bufsize, - sizeof(int)); - - if (!socket_send_all_blocking(new_fd, header, header_size, false)) - { - RARCH_ERR("%s\n", msg_hash_to_str(MSG_FAILED_TO_SEND_HEADER_TO_CLIENT)); - socket_close(new_fd); - free(header); - return; - } - - free(header); - netplay->spectate.fds[idx] = new_fd; - -#ifndef HAVE_SOCKET_LEGACY - netplay_log_connection(&their_addr, idx, netplay->other_nick); -#endif + return true; } /** - * netplay_post_frame_spectate: + * netplay_spectate_post_frame: * @netplay : pointer to netplay object * - * Post-frame for Netplay (spectate mode version). - * We check if we have new input and replay from recorded input. + * Post-frame for Netplay (spectator version). + * Not much here, just fast forward if we're behind the server. **/ static void netplay_spectate_post_frame(netplay_t *netplay) { - unsigned i; + netplay->self_ptr = NEXT_PTR(netplay->self_ptr); + netplay->self_frame_count++; - if (!netplay_is_server(netplay)) - return; - - for (i = 0; i < MAX_SPECTATORS; i++) + if (netplay_is_server(netplay)) { - char msg[128]; + /* Not expecting any client data */ + netplay->read_ptr = netplay->other_ptr = netplay->self_ptr; + netplay->read_frame_count = netplay->other_frame_count = netplay->self_frame_count; - if (netplay->spectate.fds[i] == -1) - continue; - - if (socket_send_all_blocking(netplay->spectate.fds[i], - netplay->spectate.input, - netplay->spectate.input_ptr * sizeof(int16_t), - false)) - continue; - - RARCH_LOG("Client (#%u) disconnected ...\n", i); - - snprintf(msg, sizeof(msg), "Client (#%u) disconnected.", i); - runloop_msg_queue_push(msg, 1, 180, false); - - socket_close(netplay->spectate.fds[i]); - netplay->spectate.fds[i] = -1; - break; } + else + { + /* If we must rewind, it's because we got a save state */ + if (netplay->force_rewind) + { + retro_ctx_serialize_info_t serial_info; - netplay->spectate.input_ptr = 0; + /* Replay frames. */ + netplay->is_replay = true; + netplay->replay_ptr = netplay->other_ptr; + netplay->replay_frame_count = netplay->other_frame_count; + + serial_info.data = NULL; + serial_info.data_const = netplay->buffer[netplay->replay_ptr].state; + serial_info.size = netplay->state_size; + + core_unserialize(&serial_info); + + while (netplay->replay_frame_count < netplay->self_frame_count) + { +#if defined(HAVE_THREADS) + autosave_lock(); +#endif + core_run(); +#if defined(HAVE_THREADS) + autosave_unlock(); +#endif + netplay->replay_ptr = NEXT_PTR(netplay->replay_ptr); + netplay->replay_frame_count++; + } + + netplay->is_replay = false; + netplay->force_rewind = false; + } + + /* We're in sync by definition */ + if (netplay->read_frame_count < netplay->self_frame_count) + { + netplay->other_ptr = netplay->read_ptr; + netplay->other_frame_count = netplay->read_frame_count; + } + else + { + netplay->other_ptr = netplay->self_ptr; + netplay->other_frame_count = netplay->self_frame_count; + } + + /* If the server gets significantly ahead, skip to catch up */ + if (netplay->self_frame_count + netplay->stall_frames <= netplay->read_frame_count) + { + /* "Replay" into the future */ + netplay->is_replay = true; + netplay->replay_ptr = netplay->self_ptr; + netplay->replay_frame_count = netplay->self_frame_count; + + while (netplay->replay_frame_count < netplay->read_frame_count - 1) + { +#if defined(HAVE_THREADS) + autosave_lock(); +#endif + core_run(); +#if defined(HAVE_THREADS) + autosave_unlock(); +#endif + + netplay->replay_ptr = NEXT_PTR(netplay->replay_ptr); + netplay->replay_frame_count++; + netplay->self_ptr = netplay->replay_ptr; + netplay->self_frame_count = netplay->replay_frame_count; + } + + netplay->is_replay = false; + } + + } } -static bool netplay_spectate_info_cb(netplay_t *netplay, unsigned frames) +static bool netplay_spectate_info_cb(netplay_t* netplay, unsigned frames) { - unsigned i; - if(netplay_is_server(netplay)) + if (netplay_is_server(netplay)) { - if(!netplay_get_info(netplay)) + int i; + for (i = 0; i < MAX_SPECTATORS; i++) + { + netplay->spectate.fds[i] = -1; + } + + } + else + { + if (!netplay_send_nickname(netplay, netplay->fd)) + return false; + + if (!netplay_get_nickname(netplay, netplay->fd)) return false; } - for (i = 0; i < MAX_SPECTATORS; i++) - netplay->spectate.fds[i] = -1; + netplay->has_connection = true; + return true; } @@ -186,6 +294,5 @@ struct netplay_callbacks* netplay_get_cbs_spectate(void) &netplay_spectate_post_frame, &netplay_spectate_info_cb }; - return &cbs; } diff --git a/retroarch.c b/retroarch.c index e482ab0b22..ca5559f93e 100644 --- a/retroarch.c +++ b/retroarch.c @@ -90,6 +90,7 @@ enum { RA_OPT_MENU = 256, /* must be outside the range of a char */ + RA_OPT_CHECK_FRAMES, RA_OPT_PORT, RA_OPT_SPECTATE, RA_OPT_NICK, @@ -306,6 +307,8 @@ static void retroarch_print_help(const char *arg0) puts(" -C, --connect=HOST Connect to netplay server as user 2."); puts(" --port=PORT Port used to netplay. Default is 55435."); puts(" -F, --frames=NUMBER Sync frames when using netplay."); + puts(" --check-frames=NUMBER\n" + " Check frames when using netplay."); puts(" --spectate Connect to netplay server as spectator."); #endif puts(" --nick=NICK Picks a username (for use with netplay). " @@ -677,6 +680,7 @@ static void retroarch_parse_input(int argc, char *argv[]) { "host", 0, NULL, 'H' }, { "connect", 1, NULL, 'C' }, { "frames", 1, NULL, 'F' }, + { "check-frames", 1, NULL, RA_OPT_CHECK_FRAMES }, { "port", 1, NULL, RA_OPT_PORT }, { "spectate", 0, NULL, RA_OPT_SPECTATE }, #endif @@ -978,6 +982,11 @@ static void retroarch_parse_input(int argc, char *argv[]) break; #ifdef HAVE_NETPLAY + case RA_OPT_CHECK_FRAMES: + retroarch_override_setting_set(RARCH_OVERRIDE_SETTING_NETPLAY_CHECK_FRAMES); + global->netplay.check_frames = strtoul(optarg, NULL, 0); + break; + case RA_OPT_PORT: retroarch_override_setting_set(RARCH_OVERRIDE_SETTING_NETPLAY_IP_PORT); global->netplay.port = strtoul(optarg, NULL, 0); @@ -1645,6 +1654,7 @@ static bool has_set_netplay_mode = false; static bool has_set_netplay_ip_address = false; static bool has_set_netplay_ip_port = false; static bool has_set_netplay_delay_frames= false; +static bool has_set_netplay_check_frames= false; static bool has_set_ups_pref = false; static bool has_set_bps_pref = false; static bool has_set_ips_pref = false; @@ -1671,6 +1681,8 @@ bool retroarch_override_setting_is_set(enum rarch_override_setting enum_idx) return has_set_netplay_ip_port; case RARCH_OVERRIDE_SETTING_NETPLAY_DELAY_FRAMES: return has_set_netplay_delay_frames; + case RARCH_OVERRIDE_SETTING_NETPLAY_CHECK_FRAMES: + return has_set_netplay_check_frames; case RARCH_OVERRIDE_SETTING_UPS_PREF: return has_set_ups_pref; case RARCH_OVERRIDE_SETTING_BPS_PREF: @@ -1717,6 +1729,9 @@ void retroarch_override_setting_set(enum rarch_override_setting enum_idx) case RARCH_OVERRIDE_SETTING_NETPLAY_DELAY_FRAMES: has_set_netplay_delay_frames = true; break; + case RARCH_OVERRIDE_SETTING_NETPLAY_CHECK_FRAMES: + has_set_netplay_check_frames = true; + break; case RARCH_OVERRIDE_SETTING_UPS_PREF: has_set_ups_pref = true; break; @@ -1763,6 +1778,9 @@ void retroarch_override_setting_unset(enum rarch_override_setting enum_idx) case RARCH_OVERRIDE_SETTING_NETPLAY_DELAY_FRAMES: has_set_netplay_delay_frames = false; break; + case RARCH_OVERRIDE_SETTING_NETPLAY_CHECK_FRAMES: + has_set_netplay_check_frames = false; + break; case RARCH_OVERRIDE_SETTING_UPS_PREF: has_set_ups_pref = false; break; diff --git a/retroarch.h b/retroarch.h index 878d24d756..eab78907c1 100644 --- a/retroarch.h +++ b/retroarch.h @@ -110,6 +110,7 @@ enum rarch_override_setting RARCH_OVERRIDE_SETTING_NETPLAY_IP_ADDRESS, RARCH_OVERRIDE_SETTING_NETPLAY_IP_PORT, RARCH_OVERRIDE_SETTING_NETPLAY_DELAY_FRAMES, + RARCH_OVERRIDE_SETTING_NETPLAY_CHECK_FRAMES, RARCH_OVERRIDE_SETTING_UPS_PREF, RARCH_OVERRIDE_SETTING_BPS_PREF, RARCH_OVERRIDE_SETTING_IPS_PREF, diff --git a/runloop.c b/runloop.c index 15af2333e0..fa2853ab9d 100644 --- a/runloop.c +++ b/runloop.c @@ -1577,7 +1577,13 @@ int runloop_iterate(unsigned *sleep_ms) #endif #ifdef HAVE_NETPLAY - netplay_driver_ctl(RARCH_NETPLAY_CTL_PRE_FRAME, NULL); + if (!netplay_driver_ctl(RARCH_NETPLAY_CTL_PRE_FRAME, NULL)) + { + /* Paused due to Netplay */ + core_poll(); + *sleep_ms = 10; + return 1; + } #endif if (bsv_movie_ctl(BSV_MOVIE_CTL_IS_INITED, NULL)) diff --git a/runloop.h b/runloop.h index 6bee74e3a0..b3920a9d85 100644 --- a/runloop.h +++ b/runloop.h @@ -228,6 +228,7 @@ typedef struct global bool is_client; bool is_spectate; unsigned sync_frames; + unsigned check_frames; unsigned port; } netplay; #endif