diff --git a/CMakeLists.txt b/CMakeLists.txt index aa4232c31..77a9384c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -614,6 +614,8 @@ target_sources(${PROJECT_NAME} PRIVATE core/hw/maple/maple_jvs.cpp core/hw/mem/_vmem.cpp core/hw/mem/_vmem.h + core/hw/mem/mem_watch.cpp + core/hw/mem/mem_watch.h core/hw/modem/modem.cpp core/hw/modem/modem.h core/hw/modem/modem_regs.h @@ -783,6 +785,8 @@ endif() target_sources(${PROJECT_NAME} PRIVATE core/network/dns.cpp + core/network/ggpo.cpp + core/network/ggpo.h core/network/miniupnp.cpp core/network/miniupnp.h core/network/naomi_network.cpp @@ -1115,6 +1119,45 @@ if(NOT KNOWN_ARCHITECTURE_DETECTED) endif() if(NOT LIBRETRO) + target_include_directories(${PROJECT_NAME} PRIVATE core/deps/ggpo/include core/deps/ggpo/lib/ggpo) + target_sources(${PROJECT_NAME} PRIVATE + core/deps/ggpo/lib/ggpo/bitvector.cpp + core/deps/ggpo/lib/ggpo/bitvector.h + core/deps/ggpo/lib/ggpo/game_input.cpp + core/deps/ggpo/lib/ggpo/game_input.h + core/deps/ggpo/lib/ggpo/input_queue.cpp + core/deps/ggpo/lib/ggpo/input_queue.h + core/deps/ggpo/lib/ggpo/log.cpp + core/deps/ggpo/lib/ggpo/log.h + core/deps/ggpo/lib/ggpo/main.cpp + core/deps/ggpo/lib/ggpo/platform_linux.cpp + core/deps/ggpo/lib/ggpo/platform_linux.h + core/deps/ggpo/lib/ggpo/platform_windows.cpp + core/deps/ggpo/lib/ggpo/platform_windows.h + core/deps/ggpo/lib/ggpo/poll.cpp + core/deps/ggpo/lib/ggpo/ggpo_poll.h + core/deps/ggpo/lib/ggpo/ring_buffer.h + core/deps/ggpo/lib/ggpo/static_buffer.h + core/deps/ggpo/lib/ggpo/sync.cpp + core/deps/ggpo/lib/ggpo/sync.h + core/deps/ggpo/lib/ggpo/timesync.cpp + core/deps/ggpo/lib/ggpo/timesync.h + core/deps/ggpo/lib/ggpo/ggpo_types.h + + core/deps/ggpo/lib/ggpo/backends/backend.h + core/deps/ggpo/lib/ggpo/backends/p2p.cpp + core/deps/ggpo/lib/ggpo/backends/p2p.h + core/deps/ggpo/lib/ggpo/backends/spectator.cpp + core/deps/ggpo/lib/ggpo/backends/spectator.h + core/deps/ggpo/lib/ggpo/backends/synctest.cpp + core/deps/ggpo/lib/ggpo/backends/synctest.h + + core/deps/ggpo/lib/ggpo/network/udp_msg.h + core/deps/ggpo/lib/ggpo/network/udp_proto.cpp + core/deps/ggpo/lib/ggpo/network/udp_proto.h + core/deps/ggpo/lib/ggpo/network/udp.cpp + core/deps/ggpo/lib/ggpo/network/udp.h) + if(ANDROID) target_compile_definitions(${PROJECT_NAME} PRIVATE GLES GLES3) diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index 23472a660..da7b69565 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -41,7 +41,6 @@ Option SavestateSlot("Dreamcast.SavestateSlot"); // Sound Option DSPEnabled("aica.DSPEnabled", false); -Option DisableSound("aica.NoSound"); #if HOST_CPU == CPU_ARM Option AudioBufferSize("aica.BufferSize", 5644); // 128 ms #else @@ -117,6 +116,7 @@ Option ActAsServer("ActAsServer", false, "network"); OptionString DNS("DNS", "46.101.91.123", "network"); OptionString NetworkServer("server", "", "network"); Option EmulateBBA("EmulateBBA", false, "network"); +Option GGPOEnable("GGPO", false, "network"); #ifdef SUPPORT_DISPMANX Option DispmanxMaintainAspect("maintain_aspect", true, "dispmanx"); diff --git a/core/cfg/option.h b/core/cfg/option.h index f392b2285..cb824e713 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -307,7 +307,6 @@ extern Option SavestateSlot; constexpr bool LimitFPS = true; extern Option DSPEnabled; -extern Option DisableSound; extern Option AudioBufferSize; //In samples ,*4 for bytes extern Option AutoLatency; @@ -412,6 +411,7 @@ extern Option ActAsServer; extern OptionString DNS; extern OptionString NetworkServer; extern Option EmulateBBA; +extern Option GGPOEnable; #ifdef SUPPORT_DISPMANX extern Option DispmanxMaintainAspect; diff --git a/core/deps/ggpo/include/ggponet.h b/core/deps/ggpo/include/ggponet.h new file mode 100644 index 000000000..d25516d4d --- /dev/null +++ b/core/deps/ggpo/include/ggponet.h @@ -0,0 +1,576 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _GGPONET_H_ +#define _GGPONET_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +// On windows, export at build time and import at runtime. +// ELF systems don't need an explicit export/import. +#ifdef _WIN32 +# if defined(GGPO_SHARED_LIB) +# ifdef GGPO_SDK_EXPORT +# define GGPO_API __declspec(dllexport) +# else +# define GGPO_API __declspec(dllimport) +# endif +# else +# define GGPO_API +# endif +#else +# define GGPO_API +#endif + +#ifdef __GNUC__ +#if defined(_WIN32) && !defined(_WIN64) +#define __cdecl __attribute__((cdecl)) +#elif !defined(__cdecl) +#define __cdecl +#endif +#endif + +#define GGPO_MAX_PLAYERS 4 +#define GGPO_MAX_PREDICTION_FRAMES 8 +#define GGPO_MAX_SPECTATORS 32 + +#define GGPO_SPECTATOR_INPUT_INTERVAL 4 + +typedef struct GGPOSession GGPOSession; + +typedef int GGPOPlayerHandle; + +typedef enum { + GGPO_PLAYERTYPE_LOCAL, + GGPO_PLAYERTYPE_REMOTE, + GGPO_PLAYERTYPE_SPECTATOR, +} GGPOPlayerType; + +/* + * The GGPOPlayer structure used to describe players in ggpo_add_player + * + * size: Should be set to the sizeof(GGPOPlayer) + * + * type: One of the GGPOPlayerType values describing how inputs should be handled + * Local players must have their inputs updated every frame via + * ggpo_add_local_inputs. Remote players values will come over the + * network. + * + * player_num: The player number. Should be between 1 and the number of players + * In the game (e.g. in a 2 player game, either 1 or 2). + * + * If type == GGPO_PLAYERTYPE_REMOTE: + * + * u.remote.ip_address: The ip address of the ggpo session which will host this + * player. + * + * u.remote.port: The port where udp packets should be sent to reach this player. + * All the local inputs for this session will be sent to this player at + * ip_address:port. + * + */ + +typedef struct GGPOPlayer { + int size; + GGPOPlayerType type; + int player_num; + union { + struct { + bool _unused; + } local; + struct { + char ip_address[32]; + unsigned short port; + } remote; + } u; +} GGPOPlayer; + +typedef struct GGPOLocalEndpoint { + int player_num; +} GGPOLocalEndpoint; + + +#define GGPO_ERRORLIST \ + GGPO_ERRORLIST_ENTRY(GGPO_OK, 0) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_SUCCESS, 0) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_GENERAL_FAILURE, -1) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_INVALID_SESSION, 1) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_INVALID_PLAYER_HANDLE, 2) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_PLAYER_OUT_OF_RANGE, 3) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_PREDICTION_THRESHOLD, 4) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_UNSUPPORTED, 5) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_NOT_SYNCHRONIZED, 6) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_IN_ROLLBACK, 7) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_INPUT_DROPPED, 8) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_PLAYER_DISCONNECTED, 9) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_TOO_MANY_SPECTATORS, 10) \ + GGPO_ERRORLIST_ENTRY(GGPO_ERRORCODE_INVALID_REQUEST, 11) + +#define GGPO_ERRORLIST_ENTRY(name, value) name = value, +typedef enum { + GGPO_ERRORLIST +} GGPOErrorCode; +#undef GGPO_ERRORLIST_ENTRY + +#define GGPO_SUCCEEDED(result) ((result) == GGPO_ERRORCODE_SUCCESS) + + +#define GGPO_INVALID_HANDLE (-1) + + +/* + * The GGPOEventCode enumeration describes what type of event just happened. + * + * GGPO_EVENTCODE_CONNECTED_TO_PEER - Handshake with the game running on the + * other side of the network has been completed. + * + * GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER - Beginning the synchronization + * process with the client on the other end of the networking. The count + * and total fields in the u.synchronizing struct of the GGPOEvent + * object indicate progress. + * + * GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER - The synchronziation with this + * peer has finished. + * + * GGPO_EVENTCODE_RUNNING - All the clients have synchronized. You may begin + * sending inputs with ggpo_synchronize_inputs. + * + * GGPO_EVENTCODE_DISCONNECTED_FROM_PEER - The network connection on + * the other end of the network has closed. + * + * GGPO_EVENTCODE_TIMESYNC - The time synchronziation code has determined + * that this client is too far ahead of the other one and should slow + * down to ensure fairness. The u.timesync.frames_ahead parameter in + * the GGPOEvent object indicates how many frames the client is. + * + */ +typedef enum { + GGPO_EVENTCODE_CONNECTED_TO_PEER = 1000, + GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER = 1001, + GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER = 1002, + GGPO_EVENTCODE_RUNNING = 1003, + GGPO_EVENTCODE_DISCONNECTED_FROM_PEER = 1004, + GGPO_EVENTCODE_TIMESYNC = 1005, + GGPO_EVENTCODE_CONNECTION_INTERRUPTED = 1006, + GGPO_EVENTCODE_CONNECTION_RESUMED = 1007, +} GGPOEventCode; + +/* + * The GGPOEvent structure contains an asynchronous event notification sent + * by the on_event callback. See GGPOEventCode, above, for a detailed + * explanation of each event. + */ +typedef struct { + GGPOEventCode code; + union { + struct { + GGPOPlayerHandle player; + } connected; + struct { + GGPOPlayerHandle player; + int count; + int total; + } synchronizing; + struct { + GGPOPlayerHandle player; + } synchronized; + struct { + GGPOPlayerHandle player; + } disconnected; + struct { + int frames_ahead; + } timesync; + struct { + GGPOPlayerHandle player; + int disconnect_timeout; + } connection_interrupted; + struct { + GGPOPlayerHandle player; + } connection_resumed; + } u; +} GGPOEvent; + +/* + * The GGPOSessionCallbacks structure contains the callback functions that + * your application must implement. GGPO.net will periodically call these + * functions during the game. All callback functions must be implemented. + */ +typedef struct { + /* + * begin_game callback - This callback has been deprecated. You must + * implement it, but should ignore the 'game' parameter. + */ + bool (__cdecl *begin_game)(const char *game); + + /* + * save_game_state - The client should allocate a buffer, copy the + * entire contents of the current game state into it, and copy the + * length into the *len parameter. Optionally, the client can compute + * a checksum of the data and store it in the *checksum argument. + */ + bool (__cdecl *save_game_state)(unsigned char **buffer, int *len, int *checksum, int frame); + + /* + * load_game_state - GGPO.net will call this function at the beginning + * of a rollback. The buffer and len parameters contain a previously + * saved state returned from the save_game_state function. The client + * should make the current game state match the state contained in the + * buffer. + */ + bool (__cdecl *load_game_state)(unsigned char *buffer, int len); + + /* + * log_game_state - Used in diagnostic testing. The client should use + * the ggpo_log function to write the contents of the specified save + * state in a human readible form. + */ + bool (__cdecl *log_game_state)(char *filename, unsigned char *buffer, int len); + + /* + * free_buffer - Frees a game state allocated in save_game_state. You + * should deallocate the memory contained in the buffer. + */ + void (__cdecl *free_buffer)(void *buffer); + + /* + * advance_frame - Called during a rollback. You should advance your game + * state by exactly one frame. Before each frame, call ggpo_synchronize_input + * to retrieve the inputs you should use for that frame. After each frame, + * you should call ggpo_advance_frame to notify GGPO.net that you're + * finished. + * + * The flags parameter is reserved. It can safely be ignored at this time. + */ + bool (__cdecl *advance_frame)(int flags); + + /* + * on_event - Notification that something has happened. See the GGPOEventCode + * structure above for more information. + */ + bool (__cdecl *on_event)(GGPOEvent *info); +} GGPOSessionCallbacks; + +/* + * The GGPONetworkStats function contains some statistics about the current + * session. + * + * network.send_queue_len - The length of the queue containing UDP packets + * which have not yet been acknowledged by the end client. The length of + * the send queue is a rough indication of the quality of the connection. + * The longer the send queue, the higher the round-trip time between the + * clients. The send queue will also be longer than usual during high + * packet loss situations. + * + * network.recv_queue_len - The number of inputs currently buffered by the + * GGPO.net network layer which have yet to be validated. The length of + * the prediction queue is roughly equal to the current frame number + * minus the frame number of the last packet in the remote queue. + * + * network.ping - The roundtrip packet transmission time as calcuated + * by GGPO.net. This will be roughly equal to the actual round trip + * packet transmission time + 2 the interval at which you call ggpo_idle + * or ggpo_advance_frame. + * + * network.kbps_sent - The estimated bandwidth used between the two + * clients, in kilobits per second. + * + * timesync.local_frames_behind - The number of frames GGPO.net calculates + * that the local client is behind the remote client at this instant in + * time. For example, if at this instant the current game client is running + * frame 1002 and the remote game client is running frame 1009, this value + * will mostly likely roughly equal 7. + * + * timesync.remote_frames_behind - The same as local_frames_behind, but + * calculated from the perspective of the remote player. + * + */ +typedef struct GGPONetworkStats { + struct { + int send_queue_len; + int recv_queue_len; + int ping; + int kbps_sent; + } network; + struct { + int local_frames_behind; + int remote_frames_behind; + } timesync; +} GGPONetworkStats; + +/* + * ggpo_start_session -- + * + * Used to being a new GGPO.net session. The ggpo object returned by ggpo_start_session + * uniquely identifies the state for this session and should be passed to all other + * functions. + * + * session - An out parameter to the new ggpo session object. + * + * cb - A GGPOSessionCallbacks structure which contains the callbacks you implement + * to help GGPO.net synchronize the two games. You must implement all functions in + * cb, even if they do nothing but 'return true'; + * + * game - The name of the game. This is used internally for GGPO for logging purposes only. + * + * num_players - The number of players which will be in this game. The number of players + * per session is fixed. If you need to change the number of players or any player + * disconnects, you must start a new session. + * + * input_size - The size of the game inputs which will be passsed to ggpo_add_local_input. + * + * local_port - The port GGPO should bind to for UDP traffic. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_start_session(GGPOSession **session, + GGPOSessionCallbacks *cb, + const char *game, + int num_players, + int input_size, + unsigned short localport); + + +/* + * ggpo_add_player -- + * + * Must be called for each player in the session (e.g. in a 3 player session, must + * be called 3 times). + * + * player - A GGPOPlayer struct used to describe the player. + * + * handle - An out parameter to a handle used to identify this player in the future. + * (e.g. in the on_event callbacks). + */ +GGPO_API GGPOErrorCode __cdecl ggpo_add_player(GGPOSession *session, + GGPOPlayer *player, + GGPOPlayerHandle *handle); + + +/* + * ggpo_start_synctest -- + * + * Used to being a new GGPO.net sync test session. During a sync test, every + * frame of execution is run twice: once in prediction mode and once again to + * verify the result of the prediction. If the checksums of your save states + * do not match, the test is aborted. + * + * cb - A GGPOSessionCallbacks structure which contains the callbacks you implement + * to help GGPO.net synchronize the two games. You must implement all functions in + * cb, even if they do nothing but 'return true'; + * + * game - The name of the game. This is used internally for GGPO for logging purposes only. + * + * num_players - The number of players which will be in this game. The number of players + * per session is fixed. If you need to change the number of players or any player + * disconnects, you must start a new session. + * + * input_size - The size of the game inputs which will be passsed to ggpo_add_local_input. + * + * frames - The number of frames to run before verifying the prediction. The + * recommended value is 1. + * + */ +GGPO_API GGPOErrorCode __cdecl ggpo_start_synctest(GGPOSession **session, + GGPOSessionCallbacks *cb, + const char *game, + int num_players, + int input_size, + int frames); + + +/* + * ggpo_start_spectating -- + * + * Start a spectator session. + * + * cb - A GGPOSessionCallbacks structure which contains the callbacks you implement + * to help GGPO.net synchronize the two games. You must implement all functions in + * cb, even if they do nothing but 'return true'; + * + * game - The name of the game. This is used internally for GGPO for logging purposes only. + * + * num_players - The number of players which will be in this game. The number of players + * per session is fixed. If you need to change the number of players or any player + * disconnects, you must start a new session. + * + * input_size - The size of the game inputs which will be passsed to ggpo_add_local_input. + * + * local_port - The port GGPO should bind to for UDP traffic. + * + * host_ip - The IP address of the host who will serve you the inputs for the game. Any + * player partcipating in the session can serve as a host. + * + * host_port - The port of the session on the host + */ +GGPO_API GGPOErrorCode __cdecl ggpo_start_spectating(GGPOSession **session, + GGPOSessionCallbacks *cb, + const char *game, + int num_players, + int input_size, + unsigned short local_port, + char *host_ip, + unsigned short host_port); + +/* + * ggpo_close_session -- + * Used to close a session. You must call ggpo_close_session to + * free the resources allocated in ggpo_start_session. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_close_session(GGPOSession *); + + +/* + * ggpo_set_frame_delay -- + * + * Change the amount of frames ggpo will delay local input. Must be called + * before the first call to ggpo_synchronize_input. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_set_frame_delay(GGPOSession *, + GGPOPlayerHandle player, + int frame_delay); + +/* + * ggpo_idle -- + * Should be called periodically by your application to give GGPO.net + * a chance to do some work. Most packet transmissions and rollbacks occur + * in ggpo_idle. + * + * timeout - The amount of time GGPO.net is allowed to spend in this function, + * in milliseconds. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_idle(GGPOSession *, + int timeout); + +/* + * ggpo_add_local_input -- + * + * Used to notify GGPO.net of inputs that should be trasmitted to remote + * players. ggpo_add_local_input must be called once every frame for + * all player of type GGPO_PLAYERTYPE_LOCAL. + * + * player - The player handle returned for this player when you called + * ggpo_add_local_player. + * + * values - The controller inputs for this player. + * + * size - The size of the controller inputs. This must be exactly equal to the + * size passed into ggpo_start_session. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_add_local_input(GGPOSession *, + GGPOPlayerHandle player, + void *values, + int size); + +/* + * ggpo_synchronize_input -- + * + * You should call ggpo_synchronize_input before every frame of execution, + * including those frames which happen during rollback. + * + * values - When the function returns, the values parameter will contain + * inputs for this frame for all players. The values array must be at + * least (size * players) large. + * + * size - The size of the values array. + * + * disconnect_flags - Indicated whether the input in slot (1 << flag) is + * valid. If a player has disconnected, the input in the values array for + * that player will be zeroed and the i-th flag will be set. For example, + * if only player 3 has disconnected, disconnect flags will be 8 (i.e. 1 << 3). + */ +GGPO_API GGPOErrorCode __cdecl ggpo_synchronize_input(GGPOSession *, + void *values, + int size, + int *disconnect_flags); + +/* + * ggpo_disconnect_player -- + * + * Disconnects a remote player from a game. Will return GGPO_ERRORCODE_PLAYER_DISCONNECTED + * if you try to disconnect a player who has already been disconnected. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_disconnect_player(GGPOSession *, + GGPOPlayerHandle player); + +/* + * ggpo_advance_frame -- + * + * You should call ggpo_advance_frame to notify GGPO.net that you have + * advanced your gamestate by a single frame. You should call this everytime + * you advance the gamestate by a frame, even during rollbacks. GGPO.net + * may call your save_state callback before this function returns. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_advance_frame(GGPOSession *); + +/* + * ggpo_get_network_stats -- + * + * Used to fetch some statistics about the quality of the network connection. + * + * player - The player handle returned from the ggpo_add_player function you used + * to add the remote player. + * + * stats - Out parameter to the network statistics. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_get_network_stats(GGPOSession *, + GGPOPlayerHandle player, + GGPONetworkStats *stats); + +/* + * ggpo_set_disconnect_timeout -- + * + * Sets the disconnect timeout. The session will automatically disconnect + * from a remote peer if it has not received a packet in the timeout window. + * You will be notified of the disconnect via a GGPO_EVENTCODE_DISCONNECTED_FROM_PEER + * event. + * + * Setting a timeout value of 0 will disable automatic disconnects. + * + * timeout - The time in milliseconds to wait before disconnecting a peer. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_set_disconnect_timeout(GGPOSession *, + int timeout); + +/* + * ggpo_set_disconnect_notify_start -- + * + * The time to wait before the first GGPO_EVENTCODE_NETWORK_INTERRUPTED timeout + * will be sent. + * + * timeout - The amount of time which needs to elapse without receiving a packet + * before the GGPO_EVENTCODE_NETWORK_INTERRUPTED event is sent. + */ +GGPO_API GGPOErrorCode __cdecl ggpo_set_disconnect_notify_start(GGPOSession *, + int timeout); + +/* + * ggpo_log -- + * + * Used to write to the ggpo.net log. In the current versions of the + * SDK, a log file is only generated if the "quark.log" environment + * variable is set to 1. This will change in future versions of the + * SDK. + */ +GGPO_API void __cdecl ggpo_log(GGPOSession *, + const char *fmt, ...); +/* + * ggpo_logv -- + * + * A varargs compatible version of ggpo_log. See ggpo_log for + * more details. + */ +GGPO_API void __cdecl ggpo_logv(GGPOSession *, + const char *fmt, + va_list args); + +#ifdef __cplusplus +}; +#endif + +#endif diff --git a/core/deps/ggpo/lib/ggpo/backends/backend.h b/core/deps/ggpo/lib/ggpo/backends/backend.h new file mode 100644 index 000000000..946d03997 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/backends/backend.h @@ -0,0 +1,34 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _BACKEND_H +#define _BACKEND_H + +#include "../ggpo_types.h" +#include "ggponet.h" + +struct GGPOSession { + virtual ~GGPOSession() { } + virtual GGPOErrorCode DoPoll(int timeout) { return GGPO_OK; } + virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) = 0; + virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size) = 0; + virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags) = 0; + virtual GGPOErrorCode IncrementFrame(void) { return GGPO_OK; } + virtual GGPOErrorCode Chat(char *text) { return GGPO_OK; } + virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle) { return GGPO_OK; } + virtual GGPOErrorCode GetNetworkStats(GGPONetworkStats *stats, GGPOPlayerHandle handle) { return GGPO_OK; } + virtual GGPOErrorCode Logv(const char *fmt, va_list list) { ::Logv(fmt, list); return GGPO_OK; } + + virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay) { return GGPO_ERRORCODE_UNSUPPORTED; } + virtual GGPOErrorCode SetDisconnectTimeout(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; } + virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; } +}; + +typedef struct GGPOSession Quark, IQuarkBackend; /* XXX: nuke this */ + +#endif + diff --git a/core/deps/ggpo/lib/ggpo/backends/p2p.cpp b/core/deps/ggpo/lib/ggpo/backends/p2p.cpp new file mode 100644 index 000000000..c1c71b3b2 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/backends/p2p.cpp @@ -0,0 +1,628 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "p2p.h" +#include +#include + +static const int RECOMMENDATION_INTERVAL = 240; +static const int DEFAULT_DISCONNECT_TIMEOUT = 5000; +static const int DEFAULT_DISCONNECT_NOTIFY_START = 750; + +Peer2PeerBackend::Peer2PeerBackend(GGPOSessionCallbacks *cb, + const char *gamename, + uint16 localport, + int num_players, + int input_size) : + _num_players(num_players), + _input_size(input_size), + _sync(_local_connect_status), + _disconnect_timeout(DEFAULT_DISCONNECT_TIMEOUT), + _disconnect_notify_start(DEFAULT_DISCONNECT_NOTIFY_START), + _num_spectators(0), + _next_spectator_frame(0) +{ + _callbacks = *cb; + _synchronizing = true; + _next_recommended_sleep = 0; + + /* + * Initialize the synchronziation layer + */ + Sync::Config config = { 0 }; + config.num_players = num_players; + config.input_size = input_size; + config.callbacks = _callbacks; + config.num_prediction_frames = MAX_PREDICTION_FRAMES; + _sync.Init(config); + + /* + * Initialize the UDP port + */ + _udp.Init(localport, &_poll, this); + + _endpoints = new UdpProtocol[_num_players]; + memset(_local_connect_status, 0, sizeof(_local_connect_status)); + for (int i = 0; i < ARRAY_SIZE(_local_connect_status); i++) { + _local_connect_status[i].last_frame = -1; + } + + /* + * Preload the ROM + */ + _callbacks.begin_game(gamename); +} + +Peer2PeerBackend::~Peer2PeerBackend() +{ + delete [] _endpoints; +} + +void +Peer2PeerBackend::AddRemotePlayer(char *ip, + uint16 port, + int queue) +{ + /* + * Start the state machine (xxx: no) + */ + _synchronizing = true; + + _endpoints[queue].Init(&_udp, _poll, queue, ip, port, _local_connect_status); + _endpoints[queue].SetDisconnectTimeout(_disconnect_timeout); + _endpoints[queue].SetDisconnectNotifyStart(_disconnect_notify_start); + _endpoints[queue].Synchronize(); +} + +GGPOErrorCode Peer2PeerBackend::AddSpectator(char *ip, + uint16 port) +{ + if (_num_spectators == GGPO_MAX_SPECTATORS) { + return GGPO_ERRORCODE_TOO_MANY_SPECTATORS; + } + /* + * Currently, we can only add spectators before the game starts. + */ + if (!_synchronizing) { + return GGPO_ERRORCODE_INVALID_REQUEST; + } + int queue = _num_spectators++; + + _spectators[queue].Init(&_udp, _poll, queue + 1000, ip, port, _local_connect_status); + _spectators[queue].SetDisconnectTimeout(_disconnect_timeout); + _spectators[queue].SetDisconnectNotifyStart(_disconnect_notify_start); + _spectators[queue].Synchronize(); + + return GGPO_OK; +} + +GGPOErrorCode +Peer2PeerBackend::DoPoll(int timeout) +{ + if (!_sync.InRollback()) { + _poll.Pump(0); + + PollUdpProtocolEvents(); + + if (!_synchronizing) { + _sync.CheckSimulation(timeout); + + // notify all of our endpoints of their local frame number for their + // next connection quality report + int current_frame = _sync.GetFrameCount(); + for (int i = 0; i < _num_players; i++) { + _endpoints[i].SetLocalFrameNumber(current_frame); + } + + int total_min_confirmed; + if (_num_players <= 2) { + total_min_confirmed = Poll2Players(current_frame); + } else { + total_min_confirmed = PollNPlayers(current_frame); + } + + Log("last confirmed frame in p2p backend is %d.\n", total_min_confirmed); + if (total_min_confirmed >= 0) { + ASSERT(total_min_confirmed != INT_MAX); + if (_num_spectators > 0) { + while (_next_spectator_frame <= total_min_confirmed) { + Log("pushing frame %d to spectators.\n", _next_spectator_frame); + + GameInput input; + input.frame = _next_spectator_frame; + input.size = _input_size * _num_players; + _sync.GetConfirmedInputs(input.bits, _input_size * _num_players, _next_spectator_frame); + for (int i = 0; i < _num_spectators; i++) { + _spectators[i].SendInput(input); + } + _next_spectator_frame++; + } + } + Log("setting confirmed frame in sync to %d.\n", total_min_confirmed); + _sync.SetLastConfirmedFrame(total_min_confirmed); + } + + // send timesync notifications if now is the proper time + if (current_frame > _next_recommended_sleep) { + int interval = 0; + for (int i = 0; i < _num_players; i++) { + interval = MAX(interval, _endpoints[i].RecommendFrameDelay()); + } + + if (interval > 0) { + GGPOEvent info; + info.code = GGPO_EVENTCODE_TIMESYNC; + info.u.timesync.frames_ahead = interval; + _callbacks.on_event(&info); + _next_recommended_sleep = current_frame + RECOMMENDATION_INTERVAL; + } + } + // XXX: this is obviously a farce... + if (timeout) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + return GGPO_OK; +} + +int Peer2PeerBackend::Poll2Players(int current_frame) +{ + int i; + + // discard confirmed frames as appropriate + int total_min_confirmed = MAX_INT; + for (i = 0; i < _num_players; i++) { + bool queue_connected = true; + if (_endpoints[i].IsRunning()) { + int ignore; + queue_connected = _endpoints[i].GetPeerConnectStatus(i, &ignore); + } + if (!_local_connect_status[i].disconnected) { + total_min_confirmed = MIN(_local_connect_status[i].last_frame, total_min_confirmed); + } + Log(" local endp: connected = %d, last_received = %d, total_min_confirmed = %d.\n", !_local_connect_status[i].disconnected, _local_connect_status[i].last_frame, total_min_confirmed); + if (!queue_connected && !_local_connect_status[i].disconnected) { + Log("disconnecting i %d by remote request.\n", i); + DisconnectPlayerQueue(i, total_min_confirmed); + } + Log(" total_min_confirmed = %d.\n", total_min_confirmed); + } + return total_min_confirmed; +} + +int Peer2PeerBackend::PollNPlayers(int current_frame) +{ + int i, queue, last_received; + + // discard confirmed frames as appropriate + int total_min_confirmed = MAX_INT; + for (queue = 0; queue < _num_players; queue++) { + bool queue_connected = true; + int queue_min_confirmed = MAX_INT; + Log("considering queue %d.\n", queue); + for (i = 0; i < _num_players; i++) { + // we're going to do a lot of logic here in consideration of endpoint i. + // keep accumulating the minimum confirmed point for all n*n packets and + // throw away the rest. + if (_endpoints[i].IsRunning()) { + bool connected = _endpoints[i].GetPeerConnectStatus(queue, &last_received); + + queue_connected = queue_connected && connected; + queue_min_confirmed = MIN(last_received, queue_min_confirmed); + Log(" endpoint %d: connected = %d, last_received = %d, queue_min_confirmed = %d.\n", i, connected, last_received, queue_min_confirmed); + } else { + Log(" endpoint %d: ignoring... not running.\n", i); + } + } + // merge in our local status only if we're still connected! + if (!_local_connect_status[queue].disconnected) { + queue_min_confirmed = MIN(_local_connect_status[queue].last_frame, queue_min_confirmed); + } + Log(" local endp: connected = %d, last_received = %d, queue_min_confirmed = %d.\n", !_local_connect_status[queue].disconnected, _local_connect_status[queue].last_frame, queue_min_confirmed); + + if (queue_connected) { + total_min_confirmed = MIN(queue_min_confirmed, total_min_confirmed); + } else { + // check to see if this disconnect notification is further back than we've been before. If + // so, we need to re-adjust. This can happen when we detect our own disconnect at frame n + // and later receive a disconnect notification for frame n-1. + if (!_local_connect_status[queue].disconnected || _local_connect_status[queue].last_frame > queue_min_confirmed) { + Log("disconnecting queue %d by remote request.\n", queue); + DisconnectPlayerQueue(queue, queue_min_confirmed); + } + } + Log(" total_min_confirmed = %d.\n", total_min_confirmed); + } + return total_min_confirmed; +} + + +GGPOErrorCode +Peer2PeerBackend::AddPlayer(GGPOPlayer *player, + GGPOPlayerHandle *handle) +{ + if (player->type == GGPO_PLAYERTYPE_SPECTATOR) { + return AddSpectator(player->u.remote.ip_address, player->u.remote.port); + } + + int queue = player->player_num - 1; + if (player->player_num < 1 || player->player_num > _num_players) { + return GGPO_ERRORCODE_PLAYER_OUT_OF_RANGE; + } + *handle = QueueToPlayerHandle(queue); + + if (player->type == GGPO_PLAYERTYPE_REMOTE) { + AddRemotePlayer(player->u.remote.ip_address, player->u.remote.port, queue); + } + return GGPO_OK; +} + +GGPOErrorCode +Peer2PeerBackend::AddLocalInput(GGPOPlayerHandle player, + void *values, + int size) +{ + int queue; + GameInput input; + GGPOErrorCode result; + + if (_sync.InRollback()) { + return GGPO_ERRORCODE_IN_ROLLBACK; + } + if (_synchronizing) { + return GGPO_ERRORCODE_NOT_SYNCHRONIZED; + } + + result = PlayerHandleToQueue(player, &queue); + if (!GGPO_SUCCEEDED(result)) { + return result; + } + + input.init(-1, (char *)values, size); + + // Feed the input for the current frame into the synchronzation layer. + if (!_sync.AddLocalInput(queue, input)) { + return GGPO_ERRORCODE_PREDICTION_THRESHOLD; + } + + if (input.frame != GameInput::NullFrame) { // xxx: <- comment why this is the case + // Update the local connect status state to indicate that we've got a + // confirmed local frame for this player. this must come first so it + // gets incorporated into the next packet we send. + + Log("setting local connect status for local queue %d to %d", queue, input.frame); + _local_connect_status[queue].last_frame = input.frame; + + // Send the input to all the remote players. + for (int i = 0; i < _num_players; i++) { + if (_endpoints[i].IsInitialized()) { + _endpoints[i].SendInput(input); + } + } + } + + return GGPO_OK; +} + + +GGPOErrorCode +Peer2PeerBackend::SyncInput(void *values, + int size, + int *disconnect_flags) +{ + int flags; + + // Wait until we've started to return inputs. + if (_synchronizing) { + return GGPO_ERRORCODE_NOT_SYNCHRONIZED; + } + flags = _sync.SynchronizeInputs(values, size); + if (disconnect_flags) { + *disconnect_flags = flags; + } + return GGPO_OK; +} + +GGPOErrorCode +Peer2PeerBackend::IncrementFrame(void) +{ + Log("End of frame (%d)...\n", _sync.GetFrameCount()); + _sync.IncrementFrame(); + DoPoll(0); + PollSyncEvents(); + + return GGPO_OK; +} + + +void +Peer2PeerBackend::PollSyncEvents(void) +{ + Sync::Event e; + while (_sync.GetEvent(e)) { + OnSyncEvent(e); + } + return; +} + +void +Peer2PeerBackend::PollUdpProtocolEvents(void) +{ + UdpProtocol::Event evt; + for (int i = 0; i < _num_players; i++) { + while (_endpoints[i].GetEvent(evt)) { + OnUdpProtocolPeerEvent(evt, i); + } + } + for (int i = 0; i < _num_spectators; i++) { + while (_spectators[i].GetEvent(evt)) { + OnUdpProtocolSpectatorEvent(evt, i); + } + } +} + +void +Peer2PeerBackend::OnUdpProtocolPeerEvent(UdpProtocol::Event &evt, int queue) +{ + OnUdpProtocolEvent(evt, QueueToPlayerHandle(queue)); + switch (evt.type) { + case UdpProtocol::Event::Input: + if (!_local_connect_status[queue].disconnected) { + int current_remote_frame = _local_connect_status[queue].last_frame; + int new_remote_frame = evt.u.input.input.frame; + ASSERT(current_remote_frame == -1 || new_remote_frame == (current_remote_frame + 1)); + + _sync.AddRemoteInput(queue, evt.u.input.input); + // Notify the other endpoints which frame we received from a peer + Log("setting remote connect status for queue %d to %d\n", queue, evt.u.input.input.frame); + _local_connect_status[queue].last_frame = evt.u.input.input.frame; + } + break; + + case UdpProtocol::Event::Disconnected: + DisconnectPlayer(QueueToPlayerHandle(queue)); + break; + } +} + + +void +Peer2PeerBackend::OnUdpProtocolSpectatorEvent(UdpProtocol::Event &evt, int queue) +{ + GGPOPlayerHandle handle = QueueToSpectatorHandle(queue); + OnUdpProtocolEvent(evt, handle); + + GGPOEvent info; + + switch (evt.type) { + case UdpProtocol::Event::Disconnected: + _spectators[queue].Disconnect(); + + info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER; + info.u.disconnected.player = handle; + _callbacks.on_event(&info); + + break; + } +} + +void +Peer2PeerBackend::OnUdpProtocolEvent(UdpProtocol::Event &evt, GGPOPlayerHandle handle) +{ + GGPOEvent info; + + switch (evt.type) { + case UdpProtocol::Event::Connected: + info.code = GGPO_EVENTCODE_CONNECTED_TO_PEER; + info.u.connected.player = handle; + _callbacks.on_event(&info); + break; + case UdpProtocol::Event::Synchronizing: + info.code = GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER; + info.u.synchronizing.player = handle; + info.u.synchronizing.count = evt.u.synchronizing.count; + info.u.synchronizing.total = evt.u.synchronizing.total; + _callbacks.on_event(&info); + break; + case UdpProtocol::Event::Synchronzied: + info.code = GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER; + info.u.synchronized.player = handle; + _callbacks.on_event(&info); + + CheckInitialSync(); + break; + + case UdpProtocol::Event::NetworkInterrupted: + info.code = GGPO_EVENTCODE_CONNECTION_INTERRUPTED; + info.u.connection_interrupted.player = handle; + info.u.connection_interrupted.disconnect_timeout = evt.u.network_interrupted.disconnect_timeout; + _callbacks.on_event(&info); + break; + + case UdpProtocol::Event::NetworkResumed: + info.code = GGPO_EVENTCODE_CONNECTION_RESUMED; + info.u.connection_resumed.player = handle; + _callbacks.on_event(&info); + break; + } +} + +/* + * Called only as the result of a local decision to disconnect. The remote + * decisions to disconnect are a result of us parsing the peer_connect_settings + * blob in every endpoint periodically. + */ +GGPOErrorCode +Peer2PeerBackend::DisconnectPlayer(GGPOPlayerHandle player) +{ + int queue; + GGPOErrorCode result; + + result = PlayerHandleToQueue(player, &queue); + if (!GGPO_SUCCEEDED(result)) { + return result; + } + + if (_local_connect_status[queue].disconnected) { + return GGPO_ERRORCODE_PLAYER_DISCONNECTED; + } + + if (!_endpoints[queue].IsInitialized()) { + int current_frame = _sync.GetFrameCount(); + // xxx: we should be tracking who the local player is, but for now assume + // that if the endpoint is not initalized, this must be the local player. + Log("Disconnecting local player %d at frame %d by user request.\n", queue, _local_connect_status[queue].last_frame); + for (int i = 0; i < _num_players; i++) { + if (_endpoints[i].IsInitialized()) { + DisconnectPlayerQueue(i, current_frame); + } + } + } else { + Log("Disconnecting queue %d at frame %d by user request.\n", queue, _local_connect_status[queue].last_frame); + DisconnectPlayerQueue(queue, _local_connect_status[queue].last_frame); + } + return GGPO_OK; +} + +void +Peer2PeerBackend::DisconnectPlayerQueue(int queue, int syncto) +{ + GGPOEvent info; + int framecount = _sync.GetFrameCount(); + + _endpoints[queue].Disconnect(); + + Log("Changing queue %d local connect status for last frame from %d to %d on disconnect request (current: %d).\n", + queue, _local_connect_status[queue].last_frame, syncto, framecount); + + _local_connect_status[queue].disconnected = 1; + _local_connect_status[queue].last_frame = syncto; + + if (syncto < framecount) { + Log("adjusting simulation to account for the fact that %d disconnected @ %d.\n", queue, syncto); + _sync.AdjustSimulation(syncto); + Log("finished adjusting simulation.\n"); + } + + info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER; + info.u.disconnected.player = QueueToPlayerHandle(queue); + _callbacks.on_event(&info); + + CheckInitialSync(); +} + + +GGPOErrorCode +Peer2PeerBackend::GetNetworkStats(GGPONetworkStats *stats, GGPOPlayerHandle player) +{ + int queue; + GGPOErrorCode result; + + result = PlayerHandleToQueue(player, &queue); + if (!GGPO_SUCCEEDED(result)) { + return result; + } + + memset(stats, 0, sizeof *stats); + _endpoints[queue].GetNetworkStats(stats); + + return GGPO_OK; +} + +GGPOErrorCode +Peer2PeerBackend::SetFrameDelay(GGPOPlayerHandle player, int delay) +{ + int queue; + GGPOErrorCode result; + + result = PlayerHandleToQueue(player, &queue); + if (!GGPO_SUCCEEDED(result)) { + return result; + } + _sync.SetFrameDelay(queue, delay); + return GGPO_OK; +} + +GGPOErrorCode +Peer2PeerBackend::SetDisconnectTimeout(int timeout) +{ + _disconnect_timeout = timeout; + for (int i = 0; i < _num_players; i++) { + if (_endpoints[i].IsInitialized()) { + _endpoints[i].SetDisconnectTimeout(_disconnect_timeout); + } + } + return GGPO_OK; +} + +GGPOErrorCode +Peer2PeerBackend::SetDisconnectNotifyStart(int timeout) +{ + _disconnect_notify_start = timeout; + for (int i = 0; i < _num_players; i++) { + if (_endpoints[i].IsInitialized()) { + _endpoints[i].SetDisconnectNotifyStart(_disconnect_notify_start); + } + } + return GGPO_OK; +} + +GGPOErrorCode +Peer2PeerBackend::PlayerHandleToQueue(GGPOPlayerHandle player, int *queue) +{ + int offset = ((int)player - 1); + if (offset < 0 || offset >= _num_players) { + return GGPO_ERRORCODE_INVALID_PLAYER_HANDLE; + } + *queue = offset; + return GGPO_OK; +} + + +void +Peer2PeerBackend::OnMsg(sockaddr_in &from, UdpMsg *msg, int len) +{ + for (int i = 0; i < _num_players; i++) { + if (_endpoints[i].HandlesMsg(from, msg)) { + _endpoints[i].OnMsg(msg, len); + return; + } + } + for (int i = 0; i < _num_spectators; i++) { + if (_spectators[i].HandlesMsg(from, msg)) { + _spectators[i].OnMsg(msg, len); + return; + } + } +} + +void +Peer2PeerBackend::CheckInitialSync() +{ + int i; + + if (_synchronizing) { + // Check to see if everyone is now synchronized. If so, + // go ahead and tell the client that we're ok to accept input. + for (i = 0; i < _num_players; i++) { + // xxx: IsInitialized() must go... we're actually using it as a proxy for "represents the local player" + if (_endpoints[i].IsInitialized() && !_endpoints[i].IsSynchronized() && !_local_connect_status[i].disconnected) { + return; + } + } + for (i = 0; i < _num_spectators; i++) { + if (_spectators[i].IsInitialized() && !_spectators[i].IsSynchronized()) { + return; + } + } + + GGPOEvent info; + info.code = GGPO_EVENTCODE_RUNNING; + _callbacks.on_event(&info); + _synchronizing = false; + } +} diff --git a/core/deps/ggpo/lib/ggpo/backends/p2p.h b/core/deps/ggpo/lib/ggpo/backends/p2p.h new file mode 100644 index 000000000..9b9ae71c4 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/backends/p2p.h @@ -0,0 +1,77 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _P2P_H +#define _P2P_H + +#include "ggpo_types.h" +#include "ggpo_poll.h" +#include "sync.h" +#include "backend.h" +#include "timesync.h" +#include "network/udp_proto.h" + +class Peer2PeerBackend : public IQuarkBackend, IPollSink, Udp::Callbacks { +public: + Peer2PeerBackend(GGPOSessionCallbacks *cb, const char *gamename, uint16 localport, int num_players, int input_size); + virtual ~Peer2PeerBackend(); + + +public: + virtual GGPOErrorCode DoPoll(int timeout); + virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle); + virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size); + virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags); + virtual GGPOErrorCode IncrementFrame(void); + virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle); + virtual GGPOErrorCode GetNetworkStats(GGPONetworkStats *stats, GGPOPlayerHandle handle); + virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay); + virtual GGPOErrorCode SetDisconnectTimeout(int timeout); + virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout); + +public: + virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len); + +protected: + GGPOErrorCode PlayerHandleToQueue(GGPOPlayerHandle player, int *queue); + GGPOPlayerHandle QueueToPlayerHandle(int queue) { return (GGPOPlayerHandle)(queue + 1); } + GGPOPlayerHandle QueueToSpectatorHandle(int queue) { return (GGPOPlayerHandle)(queue + 1000); } /* out of range of the player array, basically */ + void DisconnectPlayerQueue(int queue, int syncto); + void PollSyncEvents(void); + void PollUdpProtocolEvents(void); + void CheckInitialSync(void); + int Poll2Players(int current_frame); + int PollNPlayers(int current_frame); + void AddRemotePlayer(char *remoteip, uint16 reportport, int queue); + GGPOErrorCode AddSpectator(char *remoteip, uint16 reportport); + virtual void OnSyncEvent(Sync::Event &e) { } + virtual void OnUdpProtocolEvent(UdpProtocol::Event &e, GGPOPlayerHandle handle); + virtual void OnUdpProtocolPeerEvent(UdpProtocol::Event &e, int queue); + virtual void OnUdpProtocolSpectatorEvent(UdpProtocol::Event &e, int queue); + +protected: + GGPOSessionCallbacks _callbacks; + Poll _poll; + Sync _sync; + Udp _udp; + UdpProtocol *_endpoints; + UdpProtocol _spectators[GGPO_MAX_SPECTATORS]; + int _num_spectators; + int _input_size; + + bool _synchronizing; + int _num_players; + int _next_recommended_sleep; + + int _next_spectator_frame; + int _disconnect_timeout; + int _disconnect_notify_start; + + UdpMsg::connect_status _local_connect_status[UDP_MSG_MAX_PLAYERS]; +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/backends/spectator.cpp b/core/deps/ggpo/lib/ggpo/backends/spectator.cpp new file mode 100644 index 000000000..18e313045 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/backends/spectator.cpp @@ -0,0 +1,174 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "spectator.h" + +SpectatorBackend::SpectatorBackend(GGPOSessionCallbacks *cb, + const char* gamename, + uint16 localport, + int num_players, + int input_size, + char *hostip, + u_short hostport) : + _input_size(input_size), + _num_players(num_players), + _next_input_to_send(0) +{ + _callbacks = *cb; + _synchronizing = true; + + for (size_t i = 0; i < ARRAY_SIZE(_inputs); i++) { + _inputs[i].frame = -1; + } + + /* + * Initialize the UDP port + */ + _udp.Init(localport, &_poll, this); + + /* + * Init the host endpoint + */ + _host.Init(&_udp, _poll, 0, hostip, hostport, NULL); + _host.Synchronize(); + + /* + * Preload the ROM + */ + _callbacks.begin_game(gamename); +} + +SpectatorBackend::~SpectatorBackend() +{ +} + +GGPOErrorCode +SpectatorBackend::DoPoll(int timeout) +{ + _poll.Pump(0); + + PollUdpProtocolEvents(); + return GGPO_OK; +} + +GGPOErrorCode +SpectatorBackend::SyncInput(void *values, + int size, + int *disconnect_flags) +{ + // Wait until we've started to return inputs. + if (_synchronizing) { + return GGPO_ERRORCODE_NOT_SYNCHRONIZED; + } + + GameInput &input = _inputs[_next_input_to_send % SPECTATOR_FRAME_BUFFER_SIZE]; + if (input.frame < _next_input_to_send) { + // Haven't received the input from the host yet. Wait + return GGPO_ERRORCODE_PREDICTION_THRESHOLD; + } + if (input.frame > _next_input_to_send) { + // The host is way way way far ahead of the spectator. How'd this + // happen? Anyway, the input we need is gone forever. + return GGPO_ERRORCODE_GENERAL_FAILURE; + } + + ASSERT(size >= _input_size * _num_players); + memcpy(values, input.bits, _input_size * _num_players); + if (disconnect_flags) { + *disconnect_flags = 0; // xxx: should get them from the host! + } + _next_input_to_send++; + + return GGPO_OK; +} + +GGPOErrorCode +SpectatorBackend::IncrementFrame(void) +{ + Log("End of frame (%d)...\n", _next_input_to_send - 1); + DoPoll(0); + PollUdpProtocolEvents(); + + return GGPO_OK; +} + +void +SpectatorBackend::PollUdpProtocolEvents(void) +{ + UdpProtocol::Event evt; + while (_host.GetEvent(evt)) { + OnUdpProtocolEvent(evt); + } +} + +void +SpectatorBackend::OnUdpProtocolEvent(UdpProtocol::Event &evt) +{ + GGPOEvent info; + + switch (evt.type) { + case UdpProtocol::Event::Connected: + info.code = GGPO_EVENTCODE_CONNECTED_TO_PEER; + info.u.connected.player = 0; + _callbacks.on_event(&info); + break; + case UdpProtocol::Event::Synchronizing: + info.code = GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER; + info.u.synchronizing.player = 0; + info.u.synchronizing.count = evt.u.synchronizing.count; + info.u.synchronizing.total = evt.u.synchronizing.total; + _callbacks.on_event(&info); + break; + case UdpProtocol::Event::Synchronzied: + if (_synchronizing) { + info.code = GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER; + info.u.synchronized.player = 0; + _callbacks.on_event(&info); + + info.code = GGPO_EVENTCODE_RUNNING; + _callbacks.on_event(&info); + _synchronizing = false; + } + break; + + case UdpProtocol::Event::NetworkInterrupted: + info.code = GGPO_EVENTCODE_CONNECTION_INTERRUPTED; + info.u.connection_interrupted.player = 0; + info.u.connection_interrupted.disconnect_timeout = evt.u.network_interrupted.disconnect_timeout; + _callbacks.on_event(&info); + break; + + case UdpProtocol::Event::NetworkResumed: + info.code = GGPO_EVENTCODE_CONNECTION_RESUMED; + info.u.connection_resumed.player = 0; + _callbacks.on_event(&info); + break; + + case UdpProtocol::Event::Disconnected: + info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER; + info.u.disconnected.player = 0; + _callbacks.on_event(&info); + break; + + case UdpProtocol::Event::Input: + GameInput& input = evt.u.input.input; + + _host.SetLocalFrameNumber(input.frame); + _host.SendInputAck(); + _inputs[input.frame % SPECTATOR_FRAME_BUFFER_SIZE] = input; + break; + } +} + +void +SpectatorBackend::OnMsg(sockaddr_in &from, UdpMsg *msg, int len) +{ + if (_host.HandlesMsg(from, msg)) { + _host.OnMsg(msg, len); + } +} + diff --git a/core/deps/ggpo/lib/ggpo/backends/spectator.h b/core/deps/ggpo/lib/ggpo/backends/spectator.h new file mode 100644 index 000000000..51eb9c444 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/backends/spectator.h @@ -0,0 +1,59 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _SPECTATOR_H +#define _SPECTATOR_H + +#include "../ggpo_poll.h" +#include "../ggpo_types.h" +#include "sync.h" +#include "backend.h" +#include "timesync.h" +#include "network/udp_proto.h" + +#define SPECTATOR_FRAME_BUFFER_SIZE 64 + +class SpectatorBackend : public IQuarkBackend, IPollSink, Udp::Callbacks { +public: + SpectatorBackend(GGPOSessionCallbacks *cb, const char *gamename, uint16 localport, int num_players, int input_size, char *hostip, u_short hostport); + virtual ~SpectatorBackend(); + + +public: + virtual GGPOErrorCode DoPoll(int timeout); + virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) { return GGPO_ERRORCODE_UNSUPPORTED; } + virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size) { return GGPO_OK; } + virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags); + virtual GGPOErrorCode IncrementFrame(void); + virtual GGPOErrorCode DisconnectPlayer(GGPOPlayerHandle handle) { return GGPO_ERRORCODE_UNSUPPORTED; } + virtual GGPOErrorCode GetNetworkStats(GGPONetworkStats *stats, GGPOPlayerHandle handle) { return GGPO_ERRORCODE_UNSUPPORTED; } + virtual GGPOErrorCode SetFrameDelay(GGPOPlayerHandle player, int delay) { return GGPO_ERRORCODE_UNSUPPORTED; } + virtual GGPOErrorCode SetDisconnectTimeout(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; } + virtual GGPOErrorCode SetDisconnectNotifyStart(int timeout) { return GGPO_ERRORCODE_UNSUPPORTED; } + +public: + virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len); + +protected: + void PollUdpProtocolEvents(void); + void CheckInitialSync(void); + + void OnUdpProtocolEvent(UdpProtocol::Event &e); + +protected: + GGPOSessionCallbacks _callbacks; + Poll _poll; + Udp _udp; + UdpProtocol _host; + bool _synchronizing; + int _input_size; + int _num_players; + int _next_input_to_send; + GameInput _inputs[SPECTATOR_FRAME_BUFFER_SIZE]; +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/backends/synctest.cpp b/core/deps/ggpo/lib/ggpo/backends/synctest.cpp new file mode 100644 index 000000000..10e5b3147 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/backends/synctest.cpp @@ -0,0 +1,224 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "synctest.h" + +SyncTestBackend::SyncTestBackend(GGPOSessionCallbacks *cb, + const char *gamename, + int frames, + int num_players) : + _sync(NULL) +{ + _callbacks = *cb; + _num_players = num_players; + _check_distance = frames; + _last_verified = 0; + _rollingback = false; + _running = false; + _logfp = NULL; + _current_input.erase(); + strcpy(_game, gamename); + + /* + * Initialize the synchronziation layer + */ + Sync::Config config = { 0 }; + config.callbacks = _callbacks; + config.num_prediction_frames = MAX_PREDICTION_FRAMES; + _sync.Init(config); + + /* + * Preload the ROM + */ + _callbacks.begin_game(gamename); +} + +SyncTestBackend::~SyncTestBackend() +{ +} + +GGPOErrorCode +SyncTestBackend::DoPoll(int timeout) +{ + if (!_running) { + GGPOEvent info; + + info.code = GGPO_EVENTCODE_RUNNING; + _callbacks.on_event(&info); + _running = true; + } + return GGPO_OK; +} + +GGPOErrorCode +SyncTestBackend::AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle) +{ + if (player->player_num < 1 || player->player_num > _num_players) { + return GGPO_ERRORCODE_PLAYER_OUT_OF_RANGE; + } + *handle = (GGPOPlayerHandle)(player->player_num - 1); + return GGPO_OK; +} + +GGPOErrorCode +SyncTestBackend::AddLocalInput(GGPOPlayerHandle player, void *values, int size) +{ + if (!_running) { + return GGPO_ERRORCODE_NOT_SYNCHRONIZED; + } + + int index = (int)player; + for (int i = 0; i < size; i++) { + _current_input.bits[(index * size) + i] |= ((char *)values)[i]; + } + return GGPO_OK; +} + +GGPOErrorCode +SyncTestBackend::SyncInput(void *values, + int size, + int *disconnect_flags) +{ + BeginLog(false); + if (_rollingback) { + _last_input = _saved_frames.front().input; + } else { + if (_sync.GetFrameCount() == 0 && _sync.GetLastSavedFrame().buf == nullptr) { + _sync.SaveCurrentFrame(); + } + _last_input = _current_input; + } + memcpy(values, _last_input.bits, size); + if (disconnect_flags) { + *disconnect_flags = 0; + } + return GGPO_OK; +} + +GGPOErrorCode +SyncTestBackend::IncrementFrame(void) +{ + _sync.IncrementFrame(); + _current_input.erase(); + + Log("End of frame(%d)...\n", _sync.GetFrameCount()); + EndLog(); + + if (_rollingback) { + return GGPO_OK; + } + + int frame = _sync.GetFrameCount(); + // Hold onto the current frame in our queue of saved states. We'll need + // the checksum later to verify that our replay of the same frame got the + // same results. + SavedInfo info; + info.frame = frame; + info.input = _last_input; + info.cbuf = _sync.GetLastSavedFrame().cbuf; + info.buf = (char *)malloc(info.cbuf); + memcpy(info.buf, _sync.GetLastSavedFrame().buf, info.cbuf); + info.checksum = _sync.GetLastSavedFrame().checksum; + _saved_frames.push(info); + + if (frame - _last_verified == _check_distance) { + // We've gone far enough ahead and should now start replaying frames. + // Load the last verified frame and set the rollback flag to true. + _sync.LoadFrame(_last_verified); + + _rollingback = true; + while(!_saved_frames.empty()) { + _callbacks.advance_frame(0); + + // Verify that the checksumn of this frame is the same as the one in our + // list. + info = _saved_frames.front(); + _saved_frames.pop(); + + if (info.frame != _sync.GetFrameCount()) { + RaiseSyncError("Frame number %d does not match saved frame number %d", info.frame, frame); + } + int checksum = _sync.GetLastSavedFrame().checksum; + if (info.checksum != checksum) { + LogSaveStates(info); + RaiseSyncError("Checksum for frame %d does not match saved (%d != %d)", frame, checksum, info.checksum); + } + else + printf("Checksum %08d for frame %d matches.\n", checksum, info.frame); + free(info.buf); + } + _last_verified = frame; + _rollingback = false; + } + + return GGPO_OK; +} + +void +SyncTestBackend::RaiseSyncError(const char *fmt, ...) +{ + char buf[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, ARRAY_SIZE(buf), fmt, args); + va_end(args); + + puts(buf); +#ifdef _WIN32 + OutputDebugStringA(buf); +#endif + EndLog(); +// DebugBreak(); +} + +GGPOErrorCode +SyncTestBackend::Logv(char *fmt, va_list list) +{ + if (_logfp) { + vfprintf(_logfp, fmt, list); + } + return GGPO_OK; +} + +void +SyncTestBackend::BeginLog(int saving) +{ + EndLog(); + + char filename[MAX_PATH]; +#ifdef _WIN32 + CreateDirectoryA("synclogs", NULL); +#else + mkdir("synclogs", 0755); +#endif + snprintf(filename, ARRAY_SIZE(filename), "synclogs/%s-%04d-%s.log", + saving ? "state" : "log", + _sync.GetFrameCount(), + _rollingback ? "replay" : "original"); + + _logfp = fopen(filename, "w"); +} + +void +SyncTestBackend::EndLog() +{ + if (_logfp) { + fprintf(_logfp, "Closing log file.\n"); + fclose(_logfp); + _logfp = NULL; + } +} +void +SyncTestBackend::LogSaveStates(SavedInfo &info) +{ + char filename[MAX_PATH]; + snprintf(filename, ARRAY_SIZE(filename), "synclogs/state-%04d-original.log", _sync.GetFrameCount()); + _callbacks.log_game_state(filename, (unsigned char *)info.buf, info.cbuf); + + snprintf(filename, ARRAY_SIZE(filename), "synclogs/state-%04d-replay.log", _sync.GetFrameCount()); + _callbacks.log_game_state(filename, _sync.GetLastSavedFrame().buf, _sync.GetLastSavedFrame().cbuf); +} diff --git a/core/deps/ggpo/lib/ggpo/backends/synctest.h b/core/deps/ggpo/lib/ggpo/backends/synctest.h new file mode 100644 index 000000000..bd947517e --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/backends/synctest.h @@ -0,0 +1,60 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _SYNCTEST_H +#define _SYNCTEST_H + +#include "ggpo_types.h" +#include "backend.h" +#include "sync.h" + +#include "ring_buffer.h" + +class SyncTestBackend : public IQuarkBackend { +public: + SyncTestBackend(GGPOSessionCallbacks *cb, const char *gamename, int frames, int num_players); + virtual ~SyncTestBackend(); + + virtual GGPOErrorCode DoPoll(int timeout); + virtual GGPOErrorCode AddPlayer(GGPOPlayer *player, GGPOPlayerHandle *handle); + virtual GGPOErrorCode AddLocalInput(GGPOPlayerHandle player, void *values, int size); + virtual GGPOErrorCode SyncInput(void *values, int size, int *disconnect_flags); + virtual GGPOErrorCode IncrementFrame(void); + virtual GGPOErrorCode Logv(char *fmt, va_list list); + +protected: + struct SavedInfo { + int frame; + int checksum; + char *buf; + int cbuf; + GameInput input; + }; + + void RaiseSyncError(const char *fmt, ...); + void BeginLog(int saving); + void EndLog(); + void LogSaveStates(SavedInfo &info); + +protected: + GGPOSessionCallbacks _callbacks; + Sync _sync; + int _num_players; + int _check_distance; + int _last_verified; + bool _rollingback; + bool _running; + FILE *_logfp; + char _game[128]; + + GameInput _current_input; + GameInput _last_input; + RingBuffer _saved_frames; +}; + +#endif + diff --git a/core/deps/ggpo/lib/ggpo/bitvector.cpp b/core/deps/ggpo/lib/ggpo/bitvector.cpp new file mode 100644 index 000000000..183c983bb --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/bitvector.cpp @@ -0,0 +1,55 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "ggpo_types.h" +#include "bitvector.h" + +void +BitVector_SetBit(uint8 *vector, int *offset) +{ + vector[(*offset) / 8] |= (1 << ((*offset) % 8)); + *offset += 1; +} + +void +BitVector_ClearBit(uint8 *vector, int *offset) +{ + vector[(*offset) / 8] &= ~(1 << ((*offset) % 8)); + *offset += 1; +} + +void +BitVector_WriteNibblet(uint8 *vector, int nibble, int *offset) +{ + ASSERT(nibble < (1 << BITVECTOR_NIBBLE_SIZE)); + for (int i = 0; i < BITVECTOR_NIBBLE_SIZE; i++) { + if (nibble & (1 << i)) { + BitVector_SetBit(vector, offset); + } else { + BitVector_ClearBit(vector, offset); + } + } +} + +int +BitVector_ReadBit(uint8 *vector, int *offset) +{ + int retval = !!(vector[(*offset) / 8] & (1 << ((*offset) % 8))); + *offset += 1; + return retval; +} + +int +BitVector_ReadNibblet(uint8 *vector, int *offset) +{ + int nibblet = 0; + for (int i = 0; i < BITVECTOR_NIBBLE_SIZE; i++) { + nibblet |= (BitVector_ReadBit(vector, offset) << i); + } + return nibblet; +} + diff --git a/core/deps/ggpo/lib/ggpo/bitvector.h b/core/deps/ggpo/lib/ggpo/bitvector.h new file mode 100644 index 000000000..495b2306a --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/bitvector.h @@ -0,0 +1,19 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _BITVECTOR_H +#define _BITVECTOR_H + +#define BITVECTOR_NIBBLE_SIZE 8 + +void BitVector_SetBit(uint8 *vector, int *offset); +void BitVector_ClearBit(uint8 *vector, int *offset); +void BitVector_WriteNibblet(uint8 *vector, int nibble, int *offset); +int BitVector_ReadBit(uint8 *vector, int *offset); +int BitVector_ReadNibblet(uint8 *vector, int *offset); + +#endif // _BITVECTOR_H diff --git a/core/deps/ggpo/lib/ggpo/game_input.cpp b/core/deps/ggpo/lib/ggpo/game_input.cpp new file mode 100644 index 000000000..a1d62dc37 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/game_input.cpp @@ -0,0 +1,89 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "game_input.h" + +#include "ggpo_types.h" +#include "log.h" + +void +GameInput::init(int iframe, char *ibits, int isize, int offset) +{ + ASSERT(isize); + ASSERT(isize <= GAMEINPUT_MAX_BYTES); + frame = iframe; + size = isize; + memset(bits, 0, sizeof(bits)); + if (ibits) { + memcpy(bits + (offset * isize), ibits, isize); + } +} + +void +GameInput::init(int iframe, char *ibits, int isize) +{ + ASSERT(isize); + ASSERT(isize <= GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS); + frame = iframe; + size = isize; + memset(bits, 0, sizeof(bits)); + if (ibits) { + memcpy(bits, ibits, isize); + } +} + +void +GameInput::desc(char *buf, size_t buf_size, bool show_frame) const +{ + ASSERT(size); + size_t remaining = buf_size; + if (show_frame) { + remaining -= snprintf(buf, buf_size, "(frame:%d size:%d ", frame, size); + } else { + remaining -= snprintf(buf, buf_size, "(size:%d ", size); + } + + for (int i = 0; i < size * 8; i++) { + char buf2[16]; + if (value(i)) { + int c = snprintf(buf2, ARRAY_SIZE(buf2), "%2d ", i); + strncat(buf, buf2, ARRAY_SIZE(buf2)); + remaining -= c; + } + } + strcat(buf, ")"); +} + +void +GameInput::log(char *prefix, bool show_frame) const +{ + char buf[1024]; + size_t c = strlen(prefix); + strcpy(buf, prefix); + desc(buf + c, ARRAY_SIZE(buf) - c, show_frame); + strcat(buf, "\n"); + Log(buf); +} + +bool +GameInput::equal(GameInput &other, bool bitsonly) +{ + if (!bitsonly && frame != other.frame) { + Log("frames don't match: %d, %d\n", frame, other.frame); + } + if (size != other.size) { + Log("sizes don't match: %d, %d\n", size, other.size); + } + if (memcmp(bits, other.bits, size)) { + Log("bits don't match\n"); + } + ASSERT(size && other.size); + return (bitsonly || frame == other.frame) && + size == other.size && + memcmp(bits, other.bits, size) == 0; +} + diff --git a/core/deps/ggpo/lib/ggpo/game_input.h b/core/deps/ggpo/lib/ggpo/game_input.h new file mode 100644 index 000000000..bd3910ce1 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/game_input.h @@ -0,0 +1,40 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _GAMEINPUT_H +#define _GAMEINPUT_H + +#include +#include + +// GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS * 8 must be less than +// 2^BITVECTOR_NIBBLE_SIZE (see bitvector.h) + +#define GAMEINPUT_MAX_BYTES 9 +#define GAMEINPUT_MAX_PLAYERS 2 + +struct GameInput { + enum Constants { + NullFrame = -1 + }; + int frame; + int size; /* size in bytes of the entire input for all players */ + char bits[GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS]; + + bool is_null() { return frame == NullFrame; } + void init(int frame, char *bits, int size, int offset); + void init(int frame, char *bits, int size); + bool value(int i) const { return (bits[i/8] & (1 << (i%8))) != 0; } + void set(int i) { bits[i/8] |= (1 << (i%8)); } + void clear(int i) { bits[i/8] &= ~(1 << (i%8)); } + void erase() { memset(bits, 0, sizeof(bits)); } + void desc(char *buf, size_t buf_size, bool show_frame = true) const; + void log(char *prefix, bool show_frame = true) const; + bool equal(GameInput &input, bool bitsonly = false); +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/ggpo_poll.h b/core/deps/ggpo/lib/ggpo/ggpo_poll.h new file mode 100644 index 000000000..4960ebba1 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/ggpo_poll.h @@ -0,0 +1,66 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _POLL_H +#define _POLL_H + +#include "static_buffer.h" + +#define MAX_POLLABLE_HANDLES 64 + + +class IPollSink { +public: + virtual ~IPollSink() { } + virtual bool OnHandlePoll(void *) { return true; } + virtual bool OnMsgPoll(void *) { return true; } + virtual bool OnPeriodicPoll(void *, int ) { return true; } + virtual bool OnLoopPoll(void *) { return true; } +}; + +class Poll { +public: + Poll(void); +// void RegisterHandle(IPollSink *sink, HANDLE h, void *cookie = NULL); + void RegisterMsgLoop(IPollSink *sink, void *cookie = NULL); + void RegisterPeriodic(IPollSink *sink, int interval, void *cookie = NULL); + void RegisterLoop(IPollSink *sink, void *cookie = NULL); + + void Run(); + bool Pump(int timeout); + +protected: + int ComputeWaitTime(int elapsed); + + struct PollSinkCb { + IPollSink *sink; + void *cookie; + PollSinkCb() : sink(NULL), cookie(NULL) { } + PollSinkCb(IPollSink *s, void *c) : sink(s), cookie(c) { } + }; + + struct PollPeriodicSinkCb : public PollSinkCb { + int interval; + int last_fired; + PollPeriodicSinkCb() : PollSinkCb(NULL, NULL), interval(0), last_fired(0) { } + PollPeriodicSinkCb(IPollSink *s, void *c, int i) : + PollSinkCb(s, c), interval(i), last_fired(0) { } + }; + + int _start_time; + int _handle_count; +#ifdef _WIN32 + HANDLE _handles[MAX_POLLABLE_HANDLES]; +#endif + PollSinkCb _handle_sinks[MAX_POLLABLE_HANDLES]; + + StaticBuffer _msg_sinks; + StaticBuffer _loop_sinks; + StaticBuffer _periodic_sinks; +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/ggpo_types.h b/core/deps/ggpo/lib/ggpo/ggpo_types.h new file mode 100644 index 000000000..3352da7b2 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/ggpo_types.h @@ -0,0 +1,86 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _TYPES_H +#define _TYPES_H +/* + * Keep the compiler happy + */ + +#ifdef _MSC_VER +/* + * Disable specific compiler warnings + * 4018 - '<' : signed/unsigned mismatch + * 4100 - 'xxx' : unreferenced formal parameter + * 4127 - conditional expression is constant + * 4201 - nonstandard extension used : nameless struct/union + * 4389 - '!=' : signed/unsigned mismatch + * 4800 - 'int' : forcing value to bool 'true' or 'false' (performance warning) + */ +#pragma warning(disable: 4018 4100 4127 4201 4389 4800) +#endif + +/* + * Simple types + */ +typedef unsigned char uint8; +typedef unsigned short uint16; +typedef unsigned int uint32; +typedef unsigned char byte; +typedef char int8; +typedef short int16; +typedef int int32; + +/* + * Additional headers + */ +#if defined(_WIN32) +# include "platform_windows.h" +#elif defined(__unix__) || defined(__APPLE__) || defined(__SWITCH__) +# include "platform_linux.h" +#else +# error Unsupported platform +#endif + +#include "log.h" + + + +/* + * Macros + */ +#define ASSERT(x) \ + do { \ + if (!(x)) { \ + char assert_buf[1024]; \ + snprintf(assert_buf, sizeof(assert_buf) - 1, "Assertion: %s @ %s:%d (pid:%ld)", #x, __FILE__, __LINE__, (long)Platform::GetProcessID()); \ + Log("%s\n", assert_buf); \ + Log("\n"); \ + Log("\n"); \ + Log("\n"); \ + Platform::AssertFailed(assert_buf); \ + exit(0); \ + } \ + } while (false) + +#ifndef ARRAY_SIZE +# define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#endif + +#ifndef MAX_INT +# define MAX_INT 0xEFFFFFF +#endif + +#ifndef MAX +# define MAX(x, y) (((x) > (y)) ? (x) : (y)) +#endif + +#ifndef MIN +# define MIN(x, y) (((x) < (y)) ? (x) : (y)) +#endif + +#endif // _TYPES_H diff --git a/core/deps/ggpo/lib/ggpo/input_queue.cpp b/core/deps/ggpo/lib/ggpo/input_queue.cpp new file mode 100644 index 000000000..65cb26463 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/input_queue.cpp @@ -0,0 +1,320 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "input_queue.h" +#include "ggpo_types.h" + +#define PREVIOUS_FRAME(offset) (((offset) == 0) ? (INPUT_QUEUE_LENGTH - 1) : ((offset) - 1)) + +InputQueue::InputQueue(int input_size) +{ + Init(-1, input_size); +} + +InputQueue::~InputQueue() +{ +} + +void +InputQueue::Init(int id, int input_size) +{ + _id = id; + _head = 0; + _tail = 0; + _length = 0; + _frame_delay = 0; + _first_frame = true; + _last_user_added_frame = GameInput::NullFrame; + _first_incorrect_frame = GameInput::NullFrame; + _last_frame_requested = GameInput::NullFrame; + _last_added_frame = GameInput::NullFrame; + + _prediction.init(GameInput::NullFrame, NULL, input_size); + + /* + * This is safe because we know the GameInput is a proper structure (as in, + * no virtual methods, no contained classes, etc.). + */ + memset(_inputs, 0, sizeof _inputs); + for (size_t i = 0; i < ARRAY_SIZE(_inputs); i++) { + _inputs[i].size = input_size; + } +} + +int +InputQueue::GetLastConfirmedFrame() +{ + Log("returning last confirmed frame %d.\n", _last_added_frame); + return _last_added_frame; +} + +int +InputQueue::GetFirstIncorrectFrame() +{ + return _first_incorrect_frame; +} + +void +InputQueue::DiscardConfirmedFrames(int frame) +{ + ASSERT(frame >= 0); + + if (_last_frame_requested != GameInput::NullFrame) { + frame = MIN(frame, _last_frame_requested); + } + + Log("discarding confirmed frames up to %d (last_added:%d length:%d [head:%d tail:%d]).\n", + frame, _last_added_frame, _length, _head, _tail); + if (frame >= _last_added_frame) { + _tail = _head; + } else { + int offset = frame - _inputs[_tail].frame + 1; + + Log("difference of %d frames.\n", offset); + ASSERT(offset >= 0); + + _tail = (_tail + offset) % INPUT_QUEUE_LENGTH; + _length -= offset; + } + + Log("after discarding, new tail is %d (frame:%d).\n", _tail, _inputs[_tail].frame); + ASSERT(_length >= 0); +} + +void +InputQueue::ResetPrediction(int frame) +{ + ASSERT(_first_incorrect_frame == GameInput::NullFrame || frame <= _first_incorrect_frame); + + Log("resetting all prediction errors back to frame %d.\n", frame); + + /* + * There's nothing really to do other than reset our prediction + * state and the incorrect frame counter... + */ + _prediction.frame = GameInput::NullFrame; + _first_incorrect_frame = GameInput::NullFrame; + _last_frame_requested = GameInput::NullFrame; +} + +bool +InputQueue::GetConfirmedInput(int requested_frame, GameInput *input) +{ + ASSERT(_first_incorrect_frame == GameInput::NullFrame || requested_frame < _first_incorrect_frame); + int offset = requested_frame % INPUT_QUEUE_LENGTH; + if (_inputs[offset].frame != requested_frame) { + return false; + } + *input = _inputs[offset]; + return true; +} + +bool +InputQueue::GetInput(int requested_frame, GameInput *input) +{ + Log("requesting input frame %d.\n", requested_frame); + + /* + * No one should ever try to grab any input when we have a prediction + * error. Doing so means that we're just going further down the wrong + * path. ASSERT this to verify that it's true. + */ + ASSERT(_first_incorrect_frame == GameInput::NullFrame); + + /* + * Remember the last requested frame number for later. We'll need + * this in AddInput() to drop out of prediction mode. + */ + _last_frame_requested = requested_frame; + + ASSERT(requested_frame >= _inputs[_tail].frame); + + if (_prediction.frame == GameInput::NullFrame) { + /* + * If the frame requested is in our range, fetch it out of the queue and + * return it. + */ + int offset = requested_frame - _inputs[_tail].frame; + + if (offset < _length) { + offset = (offset + _tail) % INPUT_QUEUE_LENGTH; + ASSERT(_inputs[offset].frame == requested_frame); + *input = _inputs[offset]; + Log("returning confirmed frame number %d.\n", input->frame); + return true; + } + + /* + * The requested frame isn't in the queue. Bummer. This means we need + * to return a prediction frame. Predict that the user will do the + * same thing they did last time. + */ + if (requested_frame == 0) { + Log("basing new prediction frame from nothing, you're client wants frame 0.\n"); + _prediction.erase(); + } else if (_last_added_frame == GameInput::NullFrame) { + Log("basing new prediction frame from nothing, since we have no frames yet.\n"); + _prediction.erase(); + } else { + Log("basing new prediction frame from previously added frame (queue entry:%d, frame:%d).\n", + PREVIOUS_FRAME(_head), _inputs[PREVIOUS_FRAME(_head)].frame); + _prediction = _inputs[PREVIOUS_FRAME(_head)]; + } + _prediction.frame++; + } + + ASSERT(_prediction.frame >= 0); + + /* + * If we've made it this far, we must be predicting. Go ahead and + * forward the prediction frame contents. Be sure to return the + * frame number requested by the client, though. + */ + *input = _prediction; + input->frame = requested_frame; + Log("returning prediction frame number %d (%d).\n", input->frame, _prediction.frame); + + return false; +} + +void +InputQueue::AddInput(GameInput &input) +{ + int new_frame; + + Log("adding input frame number %d to queue.\n", input.frame); + + /* + * These next two lines simply verify that inputs are passed in + * sequentially by the user, regardless of frame delay. + */ + ASSERT(_last_user_added_frame == GameInput::NullFrame || + input.frame == _last_user_added_frame + 1); + _last_user_added_frame = input.frame; + + /* + * Move the queue head to the correct point in preparation to + * input the frame into the queue. + */ + new_frame = AdvanceQueueHead(input.frame); + if (new_frame != GameInput::NullFrame) { + AddDelayedInputToQueue(input, new_frame); + } + + /* + * Update the frame number for the input. This will also set the + * frame to GameInput::NullFrame for frames that get dropped (by + * design). + */ + input.frame = new_frame; +} + +void +InputQueue::AddDelayedInputToQueue(GameInput &input, int frame_number) +{ + Log("adding delayed input frame number %d to queue.\n", frame_number); + + ASSERT(input.size == _prediction.size); + + ASSERT(_last_added_frame == GameInput::NullFrame || frame_number == _last_added_frame + 1); + + ASSERT(frame_number == 0 || _inputs[PREVIOUS_FRAME(_head)].frame == frame_number - 1); + + /* + * Add the frame to the back of the queue + */ + _inputs[_head] = input; + _inputs[_head].frame = frame_number; + _head = (_head + 1) % INPUT_QUEUE_LENGTH; + _length++; + _first_frame = false; + + _last_added_frame = frame_number; + + if (_prediction.frame != GameInput::NullFrame) { + ASSERT(frame_number == _prediction.frame); + + /* + * We've been predicting... See if the inputs we've gotten match + * what we've been predicting. If so, don't worry about it. If not, + * remember the first input which was incorrect so we can report it + * in GetFirstIncorrectFrame() + */ + if (_first_incorrect_frame == GameInput::NullFrame && !_prediction.equal(input, true)) { + Log("frame %d does not match prediction. marking error.\n", frame_number); + _first_incorrect_frame = frame_number; + } + + /* + * If this input is the same frame as the last one requested and we + * still haven't found any mis-predicted inputs, we can dump out + * of predition mode entirely! Otherwise, advance the prediction frame + * count up. + */ + if (_prediction.frame == _last_frame_requested && _first_incorrect_frame == GameInput::NullFrame) { + Log("prediction is correct! dumping out of prediction mode.\n"); + _prediction.frame = GameInput::NullFrame; + } else { + _prediction.frame++; + } + } + ASSERT(_length <= INPUT_QUEUE_LENGTH); +} + +int +InputQueue::AdvanceQueueHead(int frame) +{ + Log("advancing queue head to frame %d.\n", frame); + + int expected_frame = _first_frame ? 0 : _inputs[PREVIOUS_FRAME(_head)].frame + 1; + + frame += _frame_delay; + + if (expected_frame > frame) { + /* + * This can occur when the frame delay has dropped since the last + * time we shoved a frame into the system. In this case, there's + * no room on the queue. Toss it. + */ + Log("Dropping input frame %d (expected next frame to be %d).\n", + frame, expected_frame); + return GameInput::NullFrame; + } + + while (expected_frame < frame) { + /* + * This can occur when the frame delay has been increased since the last + * time we shoved a frame into the system. We need to replicate the + * last frame in the queue several times in order to fill the space + * left. + */ + Log("Adding padding frame %d to account for change in frame delay.\n", + expected_frame); + GameInput &last_frame = _inputs[PREVIOUS_FRAME(_head)]; + AddDelayedInputToQueue(last_frame, expected_frame); + expected_frame++; + } + + ASSERT(frame == 0 || frame == _inputs[PREVIOUS_FRAME(_head)].frame + 1); + return frame; +} + + +void +InputQueue::Log(const char *fmt, ...) +{ + char buf[1024]; + size_t offset; + va_list args; + + offset = snprintf(buf, ARRAY_SIZE(buf), "input q%d | ", _id); + va_start(args, fmt); + vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args); + buf[ARRAY_SIZE(buf)-1] = '\0'; + ::Log(buf); + va_end(args); +} diff --git a/core/deps/ggpo/lib/ggpo/input_queue.h b/core/deps/ggpo/lib/ggpo/input_queue.h new file mode 100644 index 000000000..460a50168 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/input_queue.h @@ -0,0 +1,60 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _INPUT_QUEUE_H +#define _INPUT_QUEUE_H + +#include "game_input.h" + +#define INPUT_QUEUE_LENGTH 128 +#define DEFAULT_INPUT_SIZE 4 + +class InputQueue { +public: + InputQueue(int input_size = DEFAULT_INPUT_SIZE); + ~InputQueue(); + +public: + void Init(int id, int input_size); + int GetLastConfirmedFrame(); + int GetFirstIncorrectFrame(); + int GetLength() { return _length; } + + void SetFrameDelay(int delay) { _frame_delay = delay; } + void ResetPrediction(int frame); + void DiscardConfirmedFrames(int frame); + bool GetConfirmedInput(int frame, GameInput *input); + bool GetInput(int frame, GameInput *input); + void AddInput(GameInput &input); + +protected: + int AdvanceQueueHead(int frame); + void AddDelayedInputToQueue(GameInput &input, int i); + void Log(const char *fmt, ...); + +protected: + int _id; + int _head; + int _tail; + int _length; + bool _first_frame; + + int _last_user_added_frame; + int _last_added_frame; + int _first_incorrect_frame; + int _last_frame_requested; + + int _frame_delay; + + GameInput _inputs[INPUT_QUEUE_LENGTH]; + GameInput _prediction; +}; + +#endif + + + diff --git a/core/deps/ggpo/lib/ggpo/log.cpp b/core/deps/ggpo/lib/ggpo/log.cpp new file mode 100644 index 000000000..b816a05f2 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/log.cpp @@ -0,0 +1,31 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "ggpo_types.h" +#include "log/Log.h" +#include "log/LogManager.h" +#include + +void Log(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + Logv(fmt, args); + va_end(args); +} + +void Logv(const char* fmt, va_list args) +{ + std::string copy; + if (fmt[strlen(fmt) - 1] == '\n') { + copy = fmt; + copy.pop_back(); + fmt = copy.c_str(); + } + if (LogManager::GetInstance()) + LogManager::GetInstance()->Log(LogTypes::LDEBUG, LogTypes::NETWORK, __FILE__, __LINE__, fmt, args); +} diff --git a/core/deps/ggpo/lib/ggpo/log.h b/core/deps/ggpo/lib/ggpo/log.h new file mode 100644 index 000000000..0096b2416 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/log.h @@ -0,0 +1,14 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _LOG_H +#define _LOG_H + +extern void Log(const char *fmt, ...); +extern void Logv(const char *fmt, va_list list); + +#endif diff --git a/core/deps/ggpo/lib/ggpo/main.cpp b/core/deps/ggpo/lib/ggpo/main.cpp new file mode 100644 index 000000000..4da54c7e6 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/main.cpp @@ -0,0 +1,209 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "backends/p2p.h" +#include "backends/synctest.h" +#include "backends/spectator.h" +#include "ggpo_types.h" +#include "ggponet.h" + +struct Init +{ + Init() { + srand(Platform::GetCurrentTimeMS() + Platform::GetProcessID()); + } +}; +static Init init; + +void +ggpo_log(GGPOSession *ggpo, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + ggpo_logv(ggpo, fmt, args); + va_end(args); +} + +void +ggpo_logv(GGPOSession *ggpo, const char *fmt, va_list args) +{ + if (ggpo) { + ggpo->Logv(fmt, args); + } +} + +GGPOErrorCode +ggpo_start_session(GGPOSession **session, + GGPOSessionCallbacks *cb, + const char *game, + int num_players, + int input_size, + unsigned short localport) +{ + *session= (GGPOSession *)new Peer2PeerBackend(cb, + game, + localport, + num_players, + input_size); + return GGPO_OK; +} + +GGPOErrorCode +ggpo_add_player(GGPOSession *ggpo, + GGPOPlayer *player, + GGPOPlayerHandle *handle) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->AddPlayer(player, handle); +} + + + +GGPOErrorCode +ggpo_start_synctest(GGPOSession **ggpo, + GGPOSessionCallbacks *cb, + const char *game, + int num_players, + int input_size, + int frames) +{ + *ggpo = (GGPOSession *)new SyncTestBackend(cb, game, frames, num_players); + return GGPO_OK; +} + +GGPOErrorCode +ggpo_set_frame_delay(GGPOSession *ggpo, + GGPOPlayerHandle player, + int frame_delay) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->SetFrameDelay(player, frame_delay); +} + +GGPOErrorCode +ggpo_idle(GGPOSession *ggpo, int timeout) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->DoPoll(timeout); +} + +GGPOErrorCode +ggpo_add_local_input(GGPOSession *ggpo, + GGPOPlayerHandle player, + void *values, + int size) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->AddLocalInput(player, values, size); +} + +GGPOErrorCode +ggpo_synchronize_input(GGPOSession *ggpo, + void *values, + int size, + int *disconnect_flags) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->SyncInput(values, size, disconnect_flags); +} + +GGPOErrorCode ggpo_disconnect_player(GGPOSession *ggpo, + GGPOPlayerHandle player) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->DisconnectPlayer(player); +} + +GGPOErrorCode +ggpo_advance_frame(GGPOSession *ggpo) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->IncrementFrame(); +} + +GGPOErrorCode +ggpo_client_chat(GGPOSession *ggpo, char *text) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->Chat(text); +} + +GGPOErrorCode +ggpo_get_network_stats(GGPOSession *ggpo, + GGPOPlayerHandle player, + GGPONetworkStats *stats) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->GetNetworkStats(stats, player); +} + + +GGPOErrorCode +ggpo_close_session(GGPOSession *ggpo) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + delete ggpo; + return GGPO_OK; +} + +GGPOErrorCode +ggpo_set_disconnect_timeout(GGPOSession *ggpo, int timeout) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->SetDisconnectTimeout(timeout); +} + +GGPOErrorCode +ggpo_set_disconnect_notify_start(GGPOSession *ggpo, int timeout) +{ + if (!ggpo) { + return GGPO_ERRORCODE_INVALID_SESSION; + } + return ggpo->SetDisconnectNotifyStart(timeout); +} + +GGPOErrorCode ggpo_start_spectating(GGPOSession **session, + GGPOSessionCallbacks *cb, + const char *game, + int num_players, + int input_size, + unsigned short local_port, + char *host_ip, + unsigned short host_port) +{ + *session= (GGPOSession *)new SpectatorBackend(cb, + game, + local_port, + num_players, + input_size, + host_ip, + host_port); + return GGPO_OK; +} + diff --git a/core/deps/ggpo/lib/ggpo/network/udp.cpp b/core/deps/ggpo/lib/ggpo/network/udp.cpp new file mode 100644 index 000000000..5b5fbc421 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/network/udp.cpp @@ -0,0 +1,130 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "ggpo_types.h" +#include "udp.h" + +SOCKET +CreateSocket(uint16 bind_port, int retries) +{ + SOCKET s; + sockaddr_in sin; + uint16 port; + int optval = 1; + + s = socket(AF_INET, SOCK_DGRAM, 0); + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char *)&optval, sizeof optval); + optval = 0; + setsockopt(s, SOL_SOCKET, SO_LINGER, (const char *)&optval, sizeof optval); + + // non-blocking... +#ifndef _WIN32 + fcntl(s, F_SETFL, O_NONBLOCK); +#else + u_long iMode = 1; + ioctlsocket(s, FIONBIO, &iMode); +#endif + + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(INADDR_ANY); + for (port = bind_port; port <= bind_port + retries; port++) { + sin.sin_port = htons(port); + if (bind(s, (sockaddr *)&sin, sizeof sin) == 0) { + Log("Udp bound to port: %d.\n", port); + return s; + } + } + closesocket(s); + return INVALID_SOCKET; +} + +Udp::Udp() : + _socket(INVALID_SOCKET), + _callbacks(NULL) +{ +} + +Udp::~Udp(void) +{ + if (_socket != INVALID_SOCKET) { + closesocket(_socket); + _socket = INVALID_SOCKET; + } +} + +void +Udp::Init(uint16 port, Poll *poll, Callbacks *callbacks) +{ + _callbacks = callbacks; + + _poll = poll; + _poll->RegisterLoop(this); + + Log("binding udp socket to port %d.\n", port); + _socket = CreateSocket(port, 0); +} + +void +Udp::SendTo(char *buffer, int len, int flags, struct sockaddr *dst, int destlen) +{ + struct sockaddr_in *to = (struct sockaddr_in *)dst; + + int res = sendto(_socket, buffer, len, flags, dst, destlen); + if (res == SOCKET_ERROR) { + int err = WSAGetLastError(); + Log("unknown error in sendto (erro: %d wsaerr: %d).\n", res, err); + ASSERT(false && "Unknown error in sendto"); + } + char dst_ip[1024]; + Log("sent packet length %d to %s:%d (ret:%d).\n", len, inet_ntop(AF_INET, (void *)&to->sin_addr, dst_ip, ARRAY_SIZE(dst_ip)), ntohs(to->sin_port), res); +} + +bool +Udp::OnLoopPoll(void *cookie) +{ + uint8 recv_buf[MAX_UDP_PACKET_SIZE]; + sockaddr_in recv_addr; + socklen_t recv_addr_len; + + for (;;) { + recv_addr_len = sizeof(recv_addr); + int len = recvfrom(_socket, (char *)recv_buf, MAX_UDP_PACKET_SIZE, 0, (struct sockaddr *)&recv_addr, &recv_addr_len); + + // TODO: handle len == 0... indicates a disconnect. + + if (len == -1) { + int error = WSAGetLastError(); + if (error != WSAEWOULDBLOCK) { + Log("recvfrom WSAGetLastError returned %d (%x).\n", error, error); + } + break; + } else if (len > 0) { + char src_ip[1024]; + Log("recvfrom returned (len:%d from:%s:%d).\n", len, inet_ntop(AF_INET, (void*)&recv_addr.sin_addr, src_ip, ARRAY_SIZE(src_ip)), ntohs(recv_addr.sin_port) ); + UdpMsg *msg = (UdpMsg *)recv_buf; + _callbacks->OnMsg(recv_addr, msg, len); + } + } + return true; +} + + +void +Udp::Log(const char *fmt, ...) +{ + char buf[1024]; + size_t offset; + va_list args; + + strcpy(buf, "udp | "); + offset = strlen(buf); + va_start(args, fmt); + vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args); + buf[ARRAY_SIZE(buf)-1] = '\0'; + ::Log(buf); + va_end(args); +} diff --git a/core/deps/ggpo/lib/ggpo/network/udp.h b/core/deps/ggpo/lib/ggpo/network/udp.h new file mode 100644 index 000000000..529313851 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/network/udp.h @@ -0,0 +1,59 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _UDP_H +#define _UDP_H + +#include "ggpo_poll.h" +#include "udp_msg.h" +#include "ggponet.h" +#include "ring_buffer.h" + +#define MAX_UDP_ENDPOINTS 16 + +static const int MAX_UDP_PACKET_SIZE = 4096; + +class Udp : public IPollSink +{ +public: + struct Stats { + int bytes_sent; + int packets_sent; + float kbps_sent; + }; + + struct Callbacks { + virtual ~Callbacks() { } + virtual void OnMsg(sockaddr_in &from, UdpMsg *msg, int len) = 0; + }; + + +protected: + void Log(const char *fmt, ...); + +public: + Udp(); + + void Init(uint16 port, Poll *p, Callbacks *callbacks); + + void SendTo(char *buffer, int len, int flags, struct sockaddr *dst, int destlen); + + virtual bool OnLoopPoll(void *cookie); + +public: + ~Udp(void); + +protected: + // Network transmission information + SOCKET _socket; + + // state management + Callbacks *_callbacks; + Poll *_poll; +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/network/udp_msg.h b/core/deps/ggpo/lib/ggpo/network/udp_msg.h new file mode 100644 index 000000000..495a002e6 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/network/udp_msg.h @@ -0,0 +1,107 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _UDP_MSG_H +#define _UDP_MSG_H + +#define MAX_COMPRESSED_BITS 4096 +#define UDP_MSG_MAX_PLAYERS 4 + +#pragma pack(push, 1) + +struct UdpMsg +{ + enum MsgType { + Invalid = 0, + SyncRequest = 1, + SyncReply = 2, + Input = 3, + QualityReport = 4, + QualityReply = 5, + KeepAlive = 6, + InputAck = 7, + }; + + struct connect_status { + unsigned int disconnected:1; + int last_frame:31; + }; + + struct { + uint16 magic; + uint16 sequence_number; + uint8 type; /* packet type */ + } hdr; + union { + struct { + uint32 random_request; /* please reply back with this random data */ + uint16 remote_magic; + uint8 remote_endpoint; + } sync_request; + + struct { + uint32 random_reply; /* OK, here's your random data back */ + } sync_reply; + + struct { + int8 frame_advantage; /* what's the other guy's frame advantage? */ + uint32 ping; + } quality_report; + + struct { + uint32 pong; + } quality_reply; + + struct { + connect_status peer_connect_status[UDP_MSG_MAX_PLAYERS]; + + uint32 start_frame; + + int disconnect_requested:1; + int ack_frame:31; + + uint16 num_bits; + uint8 input_size; // XXX: shouldn't be in every single packet! + uint8 bits[MAX_COMPRESSED_BITS]; /* must be last */ + } input; + + struct { + int ack_frame:31; + } input_ack; + + } u; + +public: + int PacketSize() { + return sizeof(hdr) + PayloadSize(); + } + + int PayloadSize() { + int size; + + switch (hdr.type) { + case SyncRequest: return sizeof(u.sync_request); + case SyncReply: return sizeof(u.sync_reply); + case QualityReport: return sizeof(u.quality_report); + case QualityReply: return sizeof(u.quality_reply); + case InputAck: return sizeof(u.input_ack); + case KeepAlive: return 0; + case Input: + size = (int)((char *)&u.input.bits - (char *)&u.input); + size += (u.input.num_bits + 7) / 8; + return size; + } + ASSERT(false); + return 0; + } + + UdpMsg(MsgType t) { hdr.type = (uint8)t; } +}; + +#pragma pack(pop) + +#endif diff --git a/core/deps/ggpo/lib/ggpo/network/udp_proto.cpp b/core/deps/ggpo/lib/ggpo/network/udp_proto.cpp new file mode 100644 index 000000000..f180b1d6a --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/network/udp_proto.cpp @@ -0,0 +1,775 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "udp_proto.h" + +#include "../ggpo_types.h" +#include "bitvector.h" + +static const int UDP_HEADER_SIZE = 28; /* Size of IP + UDP headers */ +static const int NUM_SYNC_PACKETS = 5; +static const int SYNC_RETRY_INTERVAL = 2000; +static const int SYNC_FIRST_RETRY_INTERVAL = 500; +static const int RUNNING_RETRY_INTERVAL = 200; +static const int KEEP_ALIVE_INTERVAL = 200; +static const int QUALITY_REPORT_INTERVAL = 1000; +static const int NETWORK_STATS_INTERVAL = 1000; +static const int UDP_SHUTDOWN_TIMER = 5000; +static const int MAX_SEQ_DISTANCE = (1 << 15); + +UdpProtocol::UdpProtocol() : + _local_frame_advantage(0), + _remote_frame_advantage(0), + _queue(-1), + _magic_number(0), + _remote_magic_number(0), + _packets_sent(0), + _bytes_sent(0), + _stats_start_time(0), + _last_send_time(0), + _shutdown_timeout(0), + _disconnect_timeout(0), + _disconnect_notify_start(0), + _disconnect_notify_sent(false), + _disconnect_event_sent(false), + _connected(false), + _next_send_seq(0), + _next_recv_seq(0), + _udp(NULL) +{ + _last_sent_input.init(-1, NULL, 1); + _last_received_input.init(-1, NULL, 1); + _last_acked_input.init(-1, NULL, 1); + + memset(&_state, 0, sizeof _state); + memset(_peer_connect_status, 0, sizeof(_peer_connect_status)); + for (int i = 0; i < ARRAY_SIZE(_peer_connect_status); i++) { + _peer_connect_status[i].last_frame = -1; + } + memset(&_peer_addr, 0, sizeof _peer_addr); + _oo_packet.msg = NULL; + + _send_latency = Platform::GetConfigInt("GGPO_NETWORK_DELAY"); + _oop_percent = Platform::GetConfigInt("GGPO_OOP_PERCENT"); +} + +UdpProtocol::~UdpProtocol() +{ + ClearSendQueue(); +} + +void +UdpProtocol::Init(Udp *udp, + Poll &poll, + int queue, + char *ip, + u_short port, + UdpMsg::connect_status *status) +{ + _udp = udp; + _queue = queue; + _local_connect_status = status; + + _peer_addr.sin_family = AF_INET; + _peer_addr.sin_port = htons(port); + inet_pton(AF_INET, ip, &_peer_addr.sin_addr.s_addr); + + do { + _magic_number = (uint16)rand(); + } while (_magic_number == 0); + poll.RegisterLoop(this); +} + +void +UdpProtocol::SendInput(GameInput &input) +{ + if (_udp) { + if (_current_state == Running) { + /* + * Check to see if this is a good time to adjust for the rift... + */ + _timesync.advance_frame(input, _local_frame_advantage, _remote_frame_advantage); + + /* + * Save this input packet + * + * XXX: This queue may fill up for spectators who do not ack input packets in a timely + * manner. When this happens, we can either resize the queue (ug) or disconnect them + * (better, but still ug). For the meantime, make this queue really big to decrease + * the odds of this happening... + */ + _pending_output.push(input); + } + SendPendingOutput(); + } +} + +void +UdpProtocol::SendPendingOutput() +{ + UdpMsg *msg = new UdpMsg(UdpMsg::Input); + int i, j, offset = 0; + uint8 *bits; + GameInput last; + + if (_pending_output.size()) { + last = _last_acked_input; + bits = msg->u.input.bits; + + msg->u.input.start_frame = _pending_output.front().frame; + msg->u.input.input_size = (uint8)_pending_output.front().size; + + ASSERT(last.frame == -1 || last.frame + 1 == msg->u.input.start_frame); + for (j = 0; j < _pending_output.size(); j++) { + GameInput ¤t = _pending_output.item(j); + if (memcmp(current.bits, last.bits, current.size) != 0) { + ASSERT((GAMEINPUT_MAX_BYTES * GAMEINPUT_MAX_PLAYERS * 8) < (1 << BITVECTOR_NIBBLE_SIZE)); + for (i = 0; i < current.size * 8; i++) { + ASSERT(i < (1 << BITVECTOR_NIBBLE_SIZE)); + if (current.value(i) != last.value(i)) { + BitVector_SetBit(msg->u.input.bits, &offset); + (current.value(i) ? BitVector_SetBit : BitVector_ClearBit)(bits, &offset); + BitVector_WriteNibblet(bits, i, &offset); + } + } + } + BitVector_ClearBit(msg->u.input.bits, &offset); + last = _last_sent_input = current; + } + } else { + msg->u.input.start_frame = 0; + msg->u.input.input_size = 0; + } + msg->u.input.ack_frame = _last_received_input.frame; + msg->u.input.num_bits = (uint16)offset; + + msg->u.input.disconnect_requested = _current_state == Disconnected; + if (_local_connect_status) { + memcpy(msg->u.input.peer_connect_status, _local_connect_status, sizeof(UdpMsg::connect_status) * UDP_MSG_MAX_PLAYERS); + } else { + memset(msg->u.input.peer_connect_status, 0, sizeof(UdpMsg::connect_status) * UDP_MSG_MAX_PLAYERS); + } + + ASSERT(offset < MAX_COMPRESSED_BITS); + + SendMsg(msg); +} + +void +UdpProtocol::SendInputAck() +{ + UdpMsg *msg = new UdpMsg(UdpMsg::InputAck); + msg->u.input_ack.ack_frame = _last_received_input.frame; + SendMsg(msg); +} + +bool +UdpProtocol::GetEvent(UdpProtocol::Event &e) +{ + if (_event_queue.size() == 0) { + return false; + } + e = _event_queue.front(); + _event_queue.pop(); + return true; +} + + +bool +UdpProtocol::OnLoopPoll(void *cookie) +{ + if (!_udp) { + return true; + } + + unsigned int now = Platform::GetCurrentTimeMS(); + unsigned int next_interval; + + PumpSendQueue(); + switch (_current_state) { + case Syncing: + next_interval = (_state.sync.roundtrips_remaining == NUM_SYNC_PACKETS) ? SYNC_FIRST_RETRY_INTERVAL : SYNC_RETRY_INTERVAL; + if (_last_send_time && _last_send_time + next_interval < now) { + Log("No luck syncing after %d ms... Re-queueing sync packet.\n", next_interval); + SendSyncRequest(); + } + break; + + case Running: + // xxx: rig all this up with a timer wrapper + if (!_state.running.last_input_packet_recv_time || _state.running.last_input_packet_recv_time + RUNNING_RETRY_INTERVAL < now) { + Log("Haven't exchanged packets in a while (last received:%d last sent:%d). Resending.\n", _last_received_input.frame, _last_sent_input.frame); + SendPendingOutput(); + _state.running.last_input_packet_recv_time = now; + } + + if (!_state.running.last_quality_report_time || _state.running.last_quality_report_time + QUALITY_REPORT_INTERVAL < now) { + UdpMsg *msg = new UdpMsg(UdpMsg::QualityReport); + msg->u.quality_report.ping = Platform::GetCurrentTimeMS(); + msg->u.quality_report.frame_advantage = (uint8)_local_frame_advantage; + SendMsg(msg); + _state.running.last_quality_report_time = now; + } + + if (!_state.running.last_network_stats_interval || _state.running.last_network_stats_interval + NETWORK_STATS_INTERVAL < now) { + UpdateNetworkStats(); + _state.running.last_network_stats_interval = now; + } + + if (_last_send_time && _last_send_time + KEEP_ALIVE_INTERVAL < now) { + Log("Sending keep alive packet\n"); + SendMsg(new UdpMsg(UdpMsg::KeepAlive)); + } + + if (_disconnect_timeout && _disconnect_notify_start && + !_disconnect_notify_sent && (_last_recv_time + _disconnect_notify_start < now)) { + Log("Endpoint has stopped receiving packets for %d ms. Sending notification.\n", _disconnect_notify_start); + Event e(Event::NetworkInterrupted); + e.u.network_interrupted.disconnect_timeout = _disconnect_timeout - _disconnect_notify_start; + QueueEvent(e); + _disconnect_notify_sent = true; + } + + if (_disconnect_timeout && (_last_recv_time + _disconnect_timeout < now)) { + if (!_disconnect_event_sent) { + Log("Endpoint has stopped receiving packets for %d ms. Disconnecting.\n", _disconnect_timeout); + QueueEvent(Event(Event::Disconnected)); + _disconnect_event_sent = true; + } + } + break; + + case Disconnected: + if (_shutdown_timeout < now) { + Log("Shutting down udp connection.\n"); + _udp = NULL; + _shutdown_timeout = 0; + } + + } + + + return true; +} + +void +UdpProtocol::Disconnect() +{ + _current_state = Disconnected; + _shutdown_timeout = Platform::GetCurrentTimeMS() + UDP_SHUTDOWN_TIMER; +} + +void +UdpProtocol::SendSyncRequest() +{ + _state.sync.random = rand() & 0xFFFF; + UdpMsg *msg = new UdpMsg(UdpMsg::SyncRequest); + msg->u.sync_request.random_request = _state.sync.random; + SendMsg(msg); +} + +void +UdpProtocol::SendMsg(UdpMsg *msg) +{ + LogMsg("send", msg); + + _packets_sent++; + _last_send_time = Platform::GetCurrentTimeMS(); + _bytes_sent += msg->PacketSize(); + + msg->hdr.magic = _magic_number; + msg->hdr.sequence_number = _next_send_seq++; + + _send_queue.push(QueueEntry(Platform::GetCurrentTimeMS(), _peer_addr, msg)); + PumpSendQueue(); +} + +bool +UdpProtocol::HandlesMsg(sockaddr_in &from, + UdpMsg *msg) +{ + if (!_udp) { + return false; + } +#ifdef _WIN32 + return _peer_addr.sin_addr.S_un.S_addr == from.sin_addr.S_un.S_addr && + _peer_addr.sin_port == from.sin_port; +#else + return _peer_addr.sin_addr.s_addr == from.sin_addr.s_addr && + _peer_addr.sin_port == from.sin_port; +#endif +} + +void +UdpProtocol::OnMsg(UdpMsg *msg, int len) +{ + bool handled = false; + typedef bool (UdpProtocol::*DispatchFn)(UdpMsg *msg, int len); + static const DispatchFn table[] = { + &UdpProtocol::OnInvalid, /* Invalid */ + &UdpProtocol::OnSyncRequest, /* SyncRequest */ + &UdpProtocol::OnSyncReply, /* SyncReply */ + &UdpProtocol::OnInput, /* Input */ + &UdpProtocol::OnQualityReport, /* QualityReport */ + &UdpProtocol::OnQualityReply, /* QualityReply */ + &UdpProtocol::OnKeepAlive, /* KeepAlive */ + &UdpProtocol::OnInputAck, /* InputAck */ + }; + + // filter out messages that don't match what we expect + uint16 seq = msg->hdr.sequence_number; + if (msg->hdr.type != UdpMsg::SyncRequest && + msg->hdr.type != UdpMsg::SyncReply) { + if (msg->hdr.magic != _remote_magic_number) { + LogMsg("recv rejecting", msg); + return; + } + + // filter out out-of-order packets + uint16 skipped = (uint16)((int)seq - (int)_next_recv_seq); + // Log("checking sequence number -> next - seq : %d - %d = %d\n", seq, _next_recv_seq, skipped); + if (skipped > MAX_SEQ_DISTANCE) { + Log("dropping out of order packet (seq: %d, last seq:%d)\n", seq, _next_recv_seq); + return; + } + } + + _next_recv_seq = seq; + LogMsg("recv", msg); + if (msg->hdr.type >= ARRAY_SIZE(table)) { + OnInvalid(msg, len); + } else { + handled = (this->*(table[msg->hdr.type]))(msg, len); + } + if (handled) { + _last_recv_time = Platform::GetCurrentTimeMS(); + if (_disconnect_notify_sent && _current_state == Running) { + QueueEvent(Event(Event::NetworkResumed)); + _disconnect_notify_sent = false; + } + } +} + +void +UdpProtocol::UpdateNetworkStats(void) +{ + int now = Platform::GetCurrentTimeMS(); + + if (_stats_start_time == 0) { + _stats_start_time = now; + } + + int total_bytes_sent = _bytes_sent + (UDP_HEADER_SIZE * _packets_sent); + float seconds = (float)((now - _stats_start_time) / 1000.0); + float Bps = total_bytes_sent / seconds; + float udp_overhead = (float)(100.0 * (UDP_HEADER_SIZE * _packets_sent) / _bytes_sent); + + _kbps_sent = int(Bps / 1024); + + Log("Network Stats -- Bandwidth: %.2f KBps Packets Sent: %5d (%.2f pps) " + "KB Sent: %.2f UDP Overhead: %.2f %%.\n", + _kbps_sent, + _packets_sent, + (float)_packets_sent * 1000 / (now - _stats_start_time), + total_bytes_sent / 1024.0, + udp_overhead); +} + + +void +UdpProtocol::QueueEvent(const UdpProtocol::Event &evt) +{ + LogEvent("Queuing event", evt); + _event_queue.push(evt); +} + +void +UdpProtocol::Synchronize() +{ + if (_udp) { + _current_state = Syncing; + _state.sync.roundtrips_remaining = NUM_SYNC_PACKETS; + SendSyncRequest(); + } +} + +bool +UdpProtocol::GetPeerConnectStatus(int id, int *frame) +{ + *frame = _peer_connect_status[id].last_frame; + return !_peer_connect_status[id].disconnected; +} + +void +UdpProtocol::Log(const char *fmt, ...) +{ + char buf[1024]; + size_t offset; + va_list args; + + snprintf(buf, ARRAY_SIZE(buf), "udpproto%d | ", _queue); + offset = strlen(buf); + va_start(args, fmt); + vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args); + buf[ARRAY_SIZE(buf)-1] = '\0'; + ::Log(buf); + va_end(args); +} + +void +UdpProtocol::LogMsg(const char *prefix, UdpMsg *msg) +{ + switch (msg->hdr.type) { + case UdpMsg::SyncRequest: + Log("%s sync-request (%d).\n", prefix, + msg->u.sync_request.random_request); + break; + case UdpMsg::SyncReply: + Log("%s sync-reply (%d).\n", prefix, + msg->u.sync_reply.random_reply); + break; + case UdpMsg::QualityReport: + Log("%s quality report.\n", prefix); + break; + case UdpMsg::QualityReply: + Log("%s quality reply.\n", prefix); + break; + case UdpMsg::KeepAlive: + Log("%s keep alive.\n", prefix); + break; + case UdpMsg::Input: + Log("%s game-compressed-input %d (+ %d bits).\n", prefix, msg->u.input.start_frame, msg->u.input.num_bits); + break; + case UdpMsg::InputAck: + Log("%s input ack.\n", prefix); + break; + default: + ASSERT(false && "Unknown UdpMsg type."); + } +} + +void +UdpProtocol::LogEvent(const char *prefix, const UdpProtocol::Event &evt) +{ + switch (evt.type) { + case UdpProtocol::Event::Synchronzied: + Log("%s (event: Synchronzied).\n", prefix); + break; + } +} + +bool +UdpProtocol::OnInvalid(UdpMsg *msg, int len) +{ + ASSERT(false && "Invalid msg in UdpProtocol"); + return false; +} + +bool +UdpProtocol::OnSyncRequest(UdpMsg *msg, int len) +{ + if (_remote_magic_number != 0 && msg->hdr.magic != _remote_magic_number) { + Log("Ignoring sync request from unknown endpoint (%d != %d).\n", + msg->hdr.magic, _remote_magic_number); + return false; + } + // FIXME + //bool requeueSyncRequest = _last_send_time && _last_send_time + 20 < Platform::GetCurrentTimeMS(); + if (_state.sync.roundtrips_remaining == NUM_SYNC_PACKETS && msg->hdr.sequence_number == 0) { + Log("Sync request 0 received... Re-queueing sync packet.\n"); + SendSyncRequest(); + } + + UdpMsg *reply = new UdpMsg(UdpMsg::SyncReply); + reply->u.sync_reply.random_reply = msg->u.sync_request.random_request; + SendMsg(reply); + + return true; +} + +bool +UdpProtocol::OnSyncReply(UdpMsg *msg, int len) +{ + if (_current_state != Syncing) { + Log("Ignoring SyncReply while not synching.\n"); + return msg->hdr.magic == _remote_magic_number; + } + + if (msg->u.sync_reply.random_reply != _state.sync.random) { + Log("sync reply %d != %d. Keep looking...\n", + msg->u.sync_reply.random_reply, _state.sync.random); + return false; + } + + if (!_connected) { + QueueEvent(Event(Event::Connected)); + _connected = true; + } + + Log("Checking sync state (%d round trips remaining).\n", _state.sync.roundtrips_remaining); + if (--_state.sync.roundtrips_remaining == 0) { + Log("Synchronized!\n"); + QueueEvent(UdpProtocol::Event(UdpProtocol::Event::Synchronzied)); + _current_state = Running; + _last_received_input.frame = -1; + _remote_magic_number = msg->hdr.magic; + } else { + UdpProtocol::Event evt(UdpProtocol::Event::Synchronizing); + evt.u.synchronizing.total = NUM_SYNC_PACKETS; + evt.u.synchronizing.count = NUM_SYNC_PACKETS - _state.sync.roundtrips_remaining; + QueueEvent(evt); + SendSyncRequest(); + } + return true; +} + +bool +UdpProtocol::OnInput(UdpMsg *msg, int len) +{ + /* + * If a disconnect is requested, go ahead and disconnect now. + */ + bool disconnect_requested = msg->u.input.disconnect_requested; + if (disconnect_requested) { + if (_current_state != Disconnected && !_disconnect_event_sent) { + Log("Disconnecting endpoint on remote request.\n"); + QueueEvent(Event(Event::Disconnected)); + _disconnect_event_sent = true; + } + } else { + /* + * Update the peer connection status if this peer is still considered to be part + * of the network. + */ + UdpMsg::connect_status* remote_status = msg->u.input.peer_connect_status; + for (int i = 0; i < ARRAY_SIZE(_peer_connect_status); i++) { + ASSERT(remote_status[i].last_frame >= _peer_connect_status[i].last_frame); + _peer_connect_status[i].disconnected = _peer_connect_status[i].disconnected || remote_status[i].disconnected; + _peer_connect_status[i].last_frame = MAX(_peer_connect_status[i].last_frame, remote_status[i].last_frame); + } + } + + /* + * Decompress the input. + */ + int last_received_frame_number = _last_received_input.frame; + if (msg->u.input.num_bits) { + int offset = 0; + uint8 *bits = (uint8 *)msg->u.input.bits; + int numBits = msg->u.input.num_bits; + int currentFrame = msg->u.input.start_frame; + + _last_received_input.size = msg->u.input.input_size; + if (_last_received_input.frame < 0) { + _last_received_input.frame = msg->u.input.start_frame - 1; + } + while (offset < numBits) { + /* + * Keep walking through the frames (parsing bits) until we reach + * the inputs for the frame right after the one we're on. + */ + ASSERT(currentFrame <= (_last_received_input.frame + 1)); + bool useInputs = currentFrame == _last_received_input.frame + 1; + + while (BitVector_ReadBit(bits, &offset)) { + int on = BitVector_ReadBit(bits, &offset); + int button = BitVector_ReadNibblet(bits, &offset); + if (useInputs) { + if (on) { + _last_received_input.set(button); + } else { + _last_received_input.clear(button); + } + } + } + ASSERT(offset <= numBits); + + /* + * Now if we want to use these inputs, go ahead and send them to + * the emulator. + */ + if (useInputs) { + /* + * Move forward 1 frame in the stream. + */ + char desc[1024]; + ASSERT(currentFrame == _last_received_input.frame + 1); + _last_received_input.frame = currentFrame; + + /* + * Send the event to the emualtor + */ + UdpProtocol::Event evt(UdpProtocol::Event::Input); + evt.u.input.input = _last_received_input; + + _last_received_input.desc(desc, ARRAY_SIZE(desc)); + + _state.running.last_input_packet_recv_time = Platform::GetCurrentTimeMS(); + + Log("Sending frame %d to emu queue %d (%s).\n", _last_received_input.frame, _queue, desc); + QueueEvent(evt); + + } else { + Log("Skipping past frame:(%d) current is %d.\n", currentFrame, _last_received_input.frame); + } + + /* + * Move forward 1 frame in the input stream. + */ + currentFrame++; + } + } + ASSERT(_last_received_input.frame >= last_received_frame_number); + + /* + * Get rid of our buffered input + */ + while (_pending_output.size() && _pending_output.front().frame < msg->u.input.ack_frame) { + Log("Throwing away pending output frame %d\n", _pending_output.front().frame); + _last_acked_input = _pending_output.front(); + _pending_output.pop(); + } + return true; +} + + +bool +UdpProtocol::OnInputAck(UdpMsg *msg, int len) +{ + /* + * Get rid of our buffered input + */ + while (_pending_output.size() && _pending_output.front().frame < msg->u.input_ack.ack_frame) { + Log("Throwing away pending output frame %d\n", _pending_output.front().frame); + _last_acked_input = _pending_output.front(); + _pending_output.pop(); + } + return true; +} + +bool +UdpProtocol::OnQualityReport(UdpMsg *msg, int len) +{ + // send a reply so the other side can compute the round trip transmit time. + UdpMsg *reply = new UdpMsg(UdpMsg::QualityReply); + reply->u.quality_reply.pong = msg->u.quality_report.ping; + SendMsg(reply); + + _remote_frame_advantage = msg->u.quality_report.frame_advantage; + return true; +} + +bool +UdpProtocol::OnQualityReply(UdpMsg *msg, int len) +{ + _round_trip_time = Platform::GetCurrentTimeMS() - msg->u.quality_reply.pong; + return true; +} + +bool +UdpProtocol::OnKeepAlive(UdpMsg *msg, int len) +{ + return true; +} + +void +UdpProtocol::GetNetworkStats(struct GGPONetworkStats *s) +{ + s->network.ping = _round_trip_time; + s->network.send_queue_len = _pending_output.size(); + s->network.kbps_sent = _kbps_sent; + s->timesync.remote_frames_behind = _remote_frame_advantage; + s->timesync.local_frames_behind = _local_frame_advantage; +} + +void +UdpProtocol::SetLocalFrameNumber(int localFrame) +{ + /* + * Estimate which frame the other guy is one by looking at the + * last frame they gave us plus some delta for the one-way packet + * trip time. + */ + int remoteFrame = _last_received_input.frame + (_round_trip_time * 60 / 1000); + + /* + * Our frame advantage is how many frames *behind* the other guy + * we are. Counter-intuative, I know. It's an advantage because + * it means they'll have to predict more often and our moves will + * pop more frequenetly. + */ + _local_frame_advantage = remoteFrame - localFrame; +} + +int +UdpProtocol::RecommendFrameDelay() +{ + // XXX: require idle input should be a configuration parameter + return _timesync.recommend_frame_wait_duration(false); +} + + +void +UdpProtocol::SetDisconnectTimeout(int timeout) +{ + _disconnect_timeout = timeout; +} + +void +UdpProtocol::SetDisconnectNotifyStart(int timeout) +{ + _disconnect_notify_start = timeout; +} + +void +UdpProtocol::PumpSendQueue() +{ + while (!_send_queue.empty()) { + QueueEntry &entry = _send_queue.front(); + + if (_send_latency) { + // should really come up with a gaussian distributation based on the configured + // value, but this will do for now. + int jitter = (_send_latency * 2 / 3) + ((rand() % _send_latency) / 3); + if (Platform::GetCurrentTimeMS() < _send_queue.front().queue_time + jitter) { + break; + } + } + if (_oop_percent && !_oo_packet.msg && ((rand() % 100) < _oop_percent)) { + int delay = rand() % (_send_latency * 10 + 1000); + Log("creating rogue oop (seq: %d delay: %d)\n", entry.msg->hdr.sequence_number, delay); + _oo_packet.send_time = Platform::GetCurrentTimeMS() + delay; + _oo_packet.msg = entry.msg; + _oo_packet.dest_addr = entry.dest_addr; + } else { + ASSERT(entry.dest_addr.sin_addr.s_addr); + + _udp->SendTo((char *)entry.msg, entry.msg->PacketSize(), 0, + (struct sockaddr *)&entry.dest_addr, sizeof entry.dest_addr); + + delete entry.msg; + } + _send_queue.pop(); + } + if (_oo_packet.msg && _oo_packet.send_time < Platform::GetCurrentTimeMS()) { + Log("sending rogue oop!"); + _udp->SendTo((char *)_oo_packet.msg, _oo_packet.msg->PacketSize(), 0, + (struct sockaddr *)&_oo_packet.dest_addr, sizeof _oo_packet.dest_addr); + + delete _oo_packet.msg; + _oo_packet.msg = NULL; + } +} + +void +UdpProtocol::ClearSendQueue() +{ + while (!_send_queue.empty()) { + delete _send_queue.front().msg; + _send_queue.pop(); + } +} diff --git a/core/deps/ggpo/lib/ggpo/network/udp_proto.h b/core/deps/ggpo/lib/ggpo/network/udp_proto.h new file mode 100644 index 000000000..2adb8c6b7 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/network/udp_proto.h @@ -0,0 +1,207 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _UDP_PROTO_H_ +#define _UDP_PROTO_H_ + +#include "ggpo_poll.h" +#include "udp.h" +#include "udp_msg.h" +#include "game_input.h" +#include "timesync.h" +#include "ggponet.h" +#include "ring_buffer.h" + +class UdpProtocol : public IPollSink +{ +public: + struct Stats { + int ping; + int remote_frame_advantage; + int local_frame_advantage; + int send_queue_len; + Udp::Stats udp; + }; + + struct Event { + enum Type { + Unknown = -1, + Connected, + Synchronizing, + Synchronzied, + Input, + Disconnected, + NetworkInterrupted, + NetworkResumed, + }; + + Type type; + union { + struct { + GameInput input; + } input; + struct { + int total; + int count; + } synchronizing; + struct { + int disconnect_timeout; + } network_interrupted; + } u; + + Event(Type t = Unknown) : type(t) { } + }; + +public: + virtual bool OnLoopPoll(void *cookie); + +public: + UdpProtocol(); + virtual ~UdpProtocol(); + + void Init(Udp *udp, Poll &p, int queue, char *ip, u_short port, UdpMsg::connect_status *status); + + void Synchronize(); + bool GetPeerConnectStatus(int id, int *frame); + bool IsInitialized() { return _udp != NULL; } + bool IsSynchronized() { return _current_state == Running; } + bool IsRunning() { return _current_state == Running; } + void SendInput(GameInput &input); + void SendInputAck(); + bool HandlesMsg(sockaddr_in &from, UdpMsg *msg); + void OnMsg(UdpMsg *msg, int len); + void Disconnect(); + + void GetNetworkStats(struct GGPONetworkStats *stats); + bool GetEvent(UdpProtocol::Event &e); + void GGPONetworkStats(Stats *stats); + void SetLocalFrameNumber(int num); + int RecommendFrameDelay(); + + void SetDisconnectTimeout(int timeout); + void SetDisconnectNotifyStart(int timeout); + +protected: + enum State { + Syncing, + Synchronzied, + Running, + Disconnected + }; + struct QueueEntry { + int queue_time; + sockaddr_in dest_addr; + UdpMsg *msg; + + QueueEntry() {} + QueueEntry(int time, sockaddr_in &dst, UdpMsg *m) : queue_time(time), dest_addr(dst), msg(m) { } + }; + + bool CreateSocket(int retries); + void UpdateNetworkStats(void); + void QueueEvent(const UdpProtocol::Event &evt); + void ClearSendQueue(void); + void Log(const char *fmt, ...); + void LogMsg(const char *prefix, UdpMsg *msg); + void LogEvent(const char *prefix, const UdpProtocol::Event &evt); + void SendSyncRequest(); + void SendMsg(UdpMsg *msg); + void PumpSendQueue(); + void DispatchMsg(uint8 *buffer, int len); + void SendPendingOutput(); + bool OnInvalid(UdpMsg *msg, int len); + bool OnSyncRequest(UdpMsg *msg, int len); + bool OnSyncReply(UdpMsg *msg, int len); + bool OnInput(UdpMsg *msg, int len); + bool OnInputAck(UdpMsg *msg, int len); + bool OnQualityReport(UdpMsg *msg, int len); + bool OnQualityReply(UdpMsg *msg, int len); + bool OnKeepAlive(UdpMsg *msg, int len); + +protected: + /* + * Network transmission information + */ + Udp *_udp; + sockaddr_in _peer_addr; + uint16 _magic_number; + int _queue; + uint16 _remote_magic_number; + bool _connected; + int _send_latency; + int _oop_percent; + struct { + int send_time; + sockaddr_in dest_addr; + UdpMsg* msg; + } _oo_packet; + RingBuffer _send_queue; + + /* + * Stats + */ + int _round_trip_time; + int _packets_sent; + int _bytes_sent; + int _kbps_sent; + int _stats_start_time; + + /* + * The state machine + */ + UdpMsg::connect_status *_local_connect_status; + UdpMsg::connect_status _peer_connect_status[UDP_MSG_MAX_PLAYERS]; + + State _current_state; + union { + struct { + uint32 roundtrips_remaining; + uint32 random; + } sync; + struct { + uint32 last_quality_report_time; + uint32 last_network_stats_interval; + uint32 last_input_packet_recv_time; + } running; + } _state; + + /* + * Fairness. + */ + int _local_frame_advantage; + int _remote_frame_advantage; + + /* + * Packet loss... + */ + RingBuffer _pending_output; + GameInput _last_received_input; + GameInput _last_sent_input; + GameInput _last_acked_input; + unsigned int _last_send_time; + unsigned int _last_recv_time; + unsigned int _shutdown_timeout; + unsigned int _disconnect_event_sent; + unsigned int _disconnect_timeout; + unsigned int _disconnect_notify_start; + bool _disconnect_notify_sent; + + uint16 _next_send_seq; + uint16 _next_recv_seq; + + /* + * Rift synchronization. + */ + TimeSync _timesync; + + /* + * Event queue + */ + RingBuffer _event_queue; +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/platform_linux.cpp b/core/deps/ggpo/lib/ggpo/platform_linux.cpp new file mode 100644 index 000000000..c9645752e --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/platform_linux.cpp @@ -0,0 +1,38 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ +#if defined(__unix__) || defined(__APPLE__) || defined(__SWITCH__) + +#include "platform_linux.h" +#include +#include +#include + +uint32_t Platform::GetCurrentTimeMS() +{ + using namespace std::chrono; + static steady_clock::time_point startTime = steady_clock::now(); + + return (uint32_t)duration_cast(steady_clock::now() - startTime).count(); +} + +int Platform::GetConfigInt(const char* name) +{ + char *buf = getenv(name); + if (buf == nullptr) + return 0; + return atoi(buf); +} + +bool Platform::GetConfigBool(const char* name) +{ + char *buf = getenv(name); + if (buf == nullptr) + return false; + return atoi(buf) != 0 || strcasecmp(buf, "true") == 0; +} + +#endif diff --git a/core/deps/ggpo/lib/ggpo/platform_linux.h b/core/deps/ggpo/lib/ggpo/platform_linux.h new file mode 100644 index 000000000..9d1c517c8 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/platform_linux.h @@ -0,0 +1,65 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _GGPO_LINUX_H_ +#define _GGPO_LINUX_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using SOCKET = int; +#define closesocket close +#define INVALID_SOCKET (-1) +#define SOCKET_ERROR (-1) +#define WSAEWOULDBLOCK EWOULDBLOCK + +constexpr size_t MAX_PATH = 4096; +#ifdef INT_MAX +#undef INT_MAX +#endif +constexpr int INT_MAX = std::numeric_limits::max(); + +class Platform { +public: // types + typedef pid_t ProcessID; + +public: // functions + static ProcessID GetProcessID() { return getpid(); } + static void AssertFailed(char *msg) { fprintf(stderr, "%s", msg); } + static uint32_t GetCurrentTimeMS(); + static int GetConfigInt(const char* name); + static bool GetConfigBool(const char* name); +}; + +extern "C" { + +inline static void DebugBreak() +{ + __builtin_trap(); +} + +inline static int WSAGetLastError() { + return errno; +} + +} + +#endif diff --git a/core/deps/ggpo/lib/ggpo/platform_windows.cpp b/core/deps/ggpo/lib/ggpo/platform_windows.cpp new file mode 100644 index 000000000..59aa7cb96 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/platform_windows.cpp @@ -0,0 +1,28 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ +#ifdef _WIN32 +#include "platform_windows.h" + +int +Platform::GetConfigInt(const char* name) +{ + char buf[1024]; + if (GetEnvironmentVariable(name, buf, ARRAY_SIZE(buf)) == 0) { + return 0; + } + return atoi(buf); +} + +bool Platform::GetConfigBool(const char* name) +{ + char buf[1024]; + if (GetEnvironmentVariable(name, buf, ARRAY_SIZE(buf)) == 0) { + return false; + } + return atoi(buf) != 0 || _stricmp(buf, "true") == 0; +} +#endif diff --git a/core/deps/ggpo/lib/ggpo/platform_windows.h b/core/deps/ggpo/lib/ggpo/platform_windows.h new file mode 100644 index 000000000..20204fc34 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/platform_windows.h @@ -0,0 +1,30 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _GGPO_WINDOWS_H_ +#define _GGPO_WINDOWS_H_ + +#include +#include +#include +#include + +#include "ggpo_types.h" + +class Platform { +public: // types + typedef DWORD ProcessID; + +public: // functions + static ProcessID GetProcessID() { return GetCurrentProcessId(); } + static void AssertFailed(char *msg) { MessageBoxA(NULL, msg, "GGPO Assertion Failed", MB_OK | MB_ICONEXCLAMATION); } + static uint32 GetCurrentTimeMS() { return timeGetTime(); } + static int GetConfigInt(const char* name); + static bool GetConfigBool(const char* name); +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/poll.cpp b/core/deps/ggpo/lib/ggpo/poll.cpp new file mode 100644 index 000000000..42730b67f --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/poll.cpp @@ -0,0 +1,127 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "ggpo_poll.h" +#include "ggpo_types.h" + +#ifndef _WIN32 +#include +#include +constexpr int INFINITE = -1; +#endif + +Poll::Poll(void) : + _start_time(0), + _handle_count(0) +{ + /* + * Create a dummy handle to simplify things. + */ +#ifdef _WIN32 + _handles[_handle_count++] = CreateEvent(NULL, true, false, NULL); +#endif +} + +#if 0 +void +Poll::RegisterHandle(IPollSink *sink, HANDLE h, void *cookie) +{ + ASSERT(_handle_count < MAX_POLLABLE_HANDLES - 1); + + _handles[_handle_count] = h; + _handle_sinks[_handle_count] = PollSinkCb(sink, cookie); + _handle_count++; +} +#endif + +void +Poll::RegisterMsgLoop(IPollSink *sink, void *cookie) +{ + _msg_sinks.push_back(PollSinkCb(sink, cookie)); +} + +void +Poll::RegisterLoop(IPollSink *sink, void *cookie) +{ + _loop_sinks.push_back(PollSinkCb(sink, cookie)); +} +void +Poll::RegisterPeriodic(IPollSink *sink, int interval, void *cookie) +{ + _periodic_sinks.push_back(PollPeriodicSinkCb(sink, cookie, interval)); +} + +void +Poll::Run() +{ + while (Pump(100)) { + continue; + } +} + +bool +Poll::Pump(int timeout) +{ + int i; + bool finished = false; + + if (_start_time == 0) { + _start_time = Platform::GetCurrentTimeMS(); + } + int elapsed = Platform::GetCurrentTimeMS() - _start_time; + int maxwait = ComputeWaitTime(elapsed); + if (maxwait != INFINITE) { + timeout = MIN(timeout, maxwait); + } + +#ifdef _WIN32 + DWORD res = WaitForMultipleObjects(_handle_count, _handles, false, timeout); + if (res >= WAIT_OBJECT_0 && res < WAIT_OBJECT_0 + _handle_count) { + i = res - WAIT_OBJECT_0; + finished = !_handle_sinks[i].sink->OnHandlePoll(_handle_sinks[i].cookie) || finished; + } +#else + if (timeout > 0) + std::this_thread::sleep_for(std::chrono::milliseconds(timeout)); +#endif + for (i = 0; i < _msg_sinks.size(); i++) { + PollSinkCb &cb = _msg_sinks[i]; + finished = !cb.sink->OnMsgPoll(cb.cookie) || finished; + } + + for (i = 0; i < _periodic_sinks.size(); i++) { + PollPeriodicSinkCb &cb = _periodic_sinks[i]; + if (cb.interval + cb.last_fired <= elapsed) { + cb.last_fired = (elapsed / cb.interval) * cb.interval; + finished = !cb.sink->OnPeriodicPoll(cb.cookie, cb.last_fired) || finished; + } + } + + for (i = 0; i < _loop_sinks.size(); i++) { + PollSinkCb &cb = _loop_sinks[i]; + finished = !cb.sink->OnLoopPoll(cb.cookie) || finished; + } + return finished; +} + +int +Poll::ComputeWaitTime(int elapsed) +{ + int waitTime = INFINITE; + size_t count = _periodic_sinks.size(); + + if (count > 0) { + for (size_t i = 0; i < count; i++) { + PollPeriodicSinkCb &cb = _periodic_sinks[i]; + int timeout = (cb.interval + cb.last_fired) - elapsed; + if (waitTime == INFINITE || (timeout < waitTime)) { + waitTime = MAX(timeout, 0); + } + } + } + return waitTime; +} diff --git a/core/deps/ggpo/lib/ggpo/ring_buffer.h b/core/deps/ggpo/lib/ggpo/ring_buffer.h new file mode 100644 index 000000000..1e51f7bb1 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/ring_buffer.h @@ -0,0 +1,60 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _RING_BUFFER_H +#define _RING_BUFFER_H + +#include "ggpo_types.h" + +template class RingBuffer +{ +public: + RingBuffer() : + _head(0), + _tail(0), + _size(0) { + } + + T &front() { + ASSERT(_size != N); + return _elements[_tail]; + } + + T &item(int i) { + ASSERT(i < _size); + return _elements[(_tail + i) % N]; + } + + void pop() { + ASSERT(_size != N); + _tail = (_tail + 1) % N; + _size--; + } + + void push(const T &t) { + ASSERT(_size != (N-1)); + _elements[_head] = t; + _head = (_head + 1) % N; + _size++; + } + + int size() { + return _size; + } + + bool empty() { + return _size == 0; + } + +protected: + T _elements[N]; + int _head; + int _tail; + int _size; +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/static_buffer.h b/core/deps/ggpo/lib/ggpo/static_buffer.h new file mode 100644 index 000000000..8ff52d76a --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/static_buffer.h @@ -0,0 +1,40 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _STATIC_BUFFER_H +#define _STATIC_BUFFER_H + +#include "ggpo_types.h" + +template class StaticBuffer +{ +public: + StaticBuffer() : + _size(0) { + } + + T& operator[](int i) { + ASSERT(i >= 0 && i < _size); + return _elements[i]; + } + + void push_back(const T &t) { + ASSERT(_size != (N-1)); + _elements[_size++] = t; + } + + int size() { + return _size; + } + + +protected: + T _elements[N]; + int _size; +}; + +#endif diff --git a/core/deps/ggpo/lib/ggpo/sync.cpp b/core/deps/ggpo/lib/ggpo/sync.cpp new file mode 100644 index 000000000..c133735c3 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/sync.cpp @@ -0,0 +1,304 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "sync.h" + +Sync::Sync(UdpMsg::connect_status *connect_status) : +_input_queues(NULL), + _local_connect_status(connect_status) +{ + _framecount = 0; + _last_confirmed_frame = -1; + _max_prediction_frames = 0; + memset(&_savedstate, 0, sizeof(_savedstate)); +} + +Sync::~Sync() +{ + /* + * Delete frames manually here rather than in a destructor of the SavedFrame + * structure so we can efficently copy frames via weak references. + */ + for (size_t i = 0; i < ARRAY_SIZE(_savedstate.frames); i++) { + _callbacks.free_buffer(_savedstate.frames[i].buf); + } + delete [] _input_queues; + _input_queues = NULL; +} + +void +Sync::Init(Sync::Config &config) +{ + _config = config; + _callbacks = config.callbacks; + _framecount = 0; + _rollingback = false; + + _max_prediction_frames = config.num_prediction_frames; + + CreateQueues(config); +} + +void +Sync::SetLastConfirmedFrame(int frame) +{ + _last_confirmed_frame = frame; + if (_last_confirmed_frame > 0) { + for (int i = 0; i < _config.num_players; i++) { + _input_queues[i].DiscardConfirmedFrames(frame - 1); + } + } +} + +bool +Sync::AddLocalInput(int queue, GameInput &input) +{ + int frames_behind = _framecount - _last_confirmed_frame; + if (_framecount >= _max_prediction_frames && frames_behind >= _max_prediction_frames) { + Log("Rejecting input from emulator: reached prediction barrier.\n"); + return false; + } + + if (_framecount == 0) { + SaveCurrentFrame(); + } + + Log("Sending undelayed local frame %d to queue %d.\n", _framecount, queue); + input.frame = _framecount; + _input_queues[queue].AddInput(input); + + return true; +} + +void +Sync::AddRemoteInput(int queue, GameInput &input) +{ + _input_queues[queue].AddInput(input); +} + +int +Sync::GetConfirmedInputs(void *values, int size, int frame) +{ + int disconnect_flags = 0; + char *output = (char *)values; + + ASSERT(size >= _config.num_players * _config.input_size); + + memset(output, 0, size); + for (int i = 0; i < _config.num_players; i++) { + GameInput input; + if (_local_connect_status[i].disconnected && frame > _local_connect_status[i].last_frame) { + disconnect_flags |= (1 << i); + input.erase(); + } else { + _input_queues[i].GetConfirmedInput(frame, &input); + } + memcpy(output + (i * _config.input_size), input.bits, _config.input_size); + } + return disconnect_flags; +} + +int +Sync::SynchronizeInputs(void *values, int size) +{ + int disconnect_flags = 0; + char *output = (char *)values; + + ASSERT(size >= _config.num_players * _config.input_size); + + memset(output, 0, size); + for (int i = 0; i < _config.num_players; i++) { + GameInput input; + if (_local_connect_status[i].disconnected && _framecount > _local_connect_status[i].last_frame) { + disconnect_flags |= (1 << i); + input.erase(); + } else { + _input_queues[i].GetInput(_framecount, &input); + } + memcpy(output + (i * _config.input_size), input.bits, _config.input_size); + } + return disconnect_flags; +} + +void +Sync::CheckSimulation(int timeout) +{ + int seek_to; + if (!CheckSimulationConsistency(&seek_to)) { + AdjustSimulation(seek_to); + } +} + +void +Sync::IncrementFrame(void) +{ + _framecount++; + SaveCurrentFrame(); +} + +void +Sync::AdjustSimulation(int seek_to) +{ + int framecount = _framecount; + int count = _framecount - seek_to; + + Log("Catching up\n"); + _rollingback = true; + + /* + * Flush our input queue and load the last frame. + */ + LoadFrame(seek_to); + ASSERT(_framecount == seek_to); + + /* + * Advance frame by frame (stuffing notifications back to + * the master). + */ + ResetPrediction(_framecount); + for (int i = 0; i < count; i++) { + _callbacks.advance_frame(0); + } + ASSERT(_framecount == framecount); + + _rollingback = false; + + Log("---\n"); +} + +void +Sync::LoadFrame(int frame) +{ + // find the frame in question + if (frame == _framecount) { + Log("Skipping NOP.\n"); + return; + } + + // Move the head pointer back and load it up + _savedstate.head = FindSavedFrameIndex(frame); + SavedFrame *state = _savedstate.frames + _savedstate.head; + + Log("=== Loading frame info %d (size: %d checksum: %08x).\n", + state->frame, state->cbuf, state->checksum); + + ASSERT(state->buf && state->cbuf); + _callbacks.load_game_state(state->buf, state->cbuf); + + // Reset framecount and the head of the state ring-buffer to point in + // advance of the current frame (as if we had just finished executing it). + _framecount = state->frame; + _savedstate.head = (_savedstate.head + 1) % ARRAY_SIZE(_savedstate.frames); +} + +void +Sync::SaveCurrentFrame() +{ + /* + * See StateCompress for the real save feature implemented by FinalBurn. + * Write everything into the head, then advance the head pointer. + */ + SavedFrame *state = _savedstate.frames + _savedstate.head; + if (state->buf) { + _callbacks.free_buffer(state->buf); + state->buf = NULL; + } + state->frame = _framecount; + _callbacks.save_game_state(&state->buf, &state->cbuf, &state->checksum, state->frame); + + Log("=== Saved frame info %d (size: %d checksum: %08x).\n", state->frame, state->cbuf, state->checksum); + _savedstate.head = (_savedstate.head + 1) % ARRAY_SIZE(_savedstate.frames); +} + +Sync::SavedFrame& +Sync::GetLastSavedFrame() +{ + int i = _savedstate.head - 1; + if (i < 0) { + i = ARRAY_SIZE(_savedstate.frames) - 1; + } + return _savedstate.frames[i]; +} + + +int +Sync::FindSavedFrameIndex(int frame) +{ + int i, count = ARRAY_SIZE(_savedstate.frames); + for (i = 0; i < count; i++) { + if (_savedstate.frames[i].frame == frame) { + break; + } + } + if (i == count) { + ASSERT(false); + } + return i; +} + + +bool +Sync::CreateQueues(Config &config) +{ + delete [] _input_queues; + _input_queues = new InputQueue[_config.num_players]; + + for (int i = 0; i < _config.num_players; i++) { + _input_queues[i].Init(i, _config.input_size); + } + return true; +} + +bool +Sync::CheckSimulationConsistency(int *seekTo) +{ + int first_incorrect = GameInput::NullFrame; + for (int i = 0; i < _config.num_players; i++) { + int incorrect = _input_queues[i].GetFirstIncorrectFrame(); + Log("considering incorrect frame %d reported by queue %d.\n", incorrect, i); + + if (incorrect != GameInput::NullFrame && (first_incorrect == GameInput::NullFrame || incorrect < first_incorrect)) { + first_incorrect = incorrect; + } + } + + if (first_incorrect == GameInput::NullFrame) { + Log("prediction ok. proceeding.\n"); + return true; + } + *seekTo = first_incorrect; + return false; +} + +void +Sync::SetFrameDelay(int queue, int delay) +{ + _input_queues[queue].SetFrameDelay(delay); +} + + +void +Sync::ResetPrediction(int frameNumber) +{ + for (int i = 0; i < _config.num_players; i++) { + _input_queues[i].ResetPrediction(frameNumber); + } +} + + +bool +Sync::GetEvent(Event &e) +{ + if (_event_queue.size()) { + e = _event_queue.front(); + _event_queue.pop(); + return true; + } + return false; +} + + diff --git a/core/deps/ggpo/lib/ggpo/sync.h b/core/deps/ggpo/lib/ggpo/sync.h new file mode 100644 index 000000000..465ee248b --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/sync.h @@ -0,0 +1,104 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _SYNC_H +#define _SYNC_H + +#include "ggponet.h" +#include "game_input.h" +#include "ggpo_types.h" +#include "input_queue.h" +#include "ring_buffer.h" +#include "network/udp_msg.h" + +#define MAX_PREDICTION_FRAMES 8 + +class SyncTestBackend; + +class Sync { +public: + struct Config { + GGPOSessionCallbacks callbacks; + int num_prediction_frames; + int num_players; + int input_size; + }; + struct Event { + enum { + ConfirmedInput, + } type; + union { + struct { + GameInput input; + } confirmedInput; + } u; + }; + +public: + Sync(UdpMsg::connect_status *connect_status); + virtual ~Sync(); + + void Init(Config &config); + + void SetLastConfirmedFrame(int frame); + void SetFrameDelay(int queue, int delay); + bool AddLocalInput(int queue, GameInput &input); + void AddRemoteInput(int queue, GameInput &input); + int GetConfirmedInputs(void *values, int size, int frame); + int SynchronizeInputs(void *values, int size); + + void CheckSimulation(int timeout); + void AdjustSimulation(int seek_to); + void IncrementFrame(void); + + int GetFrameCount() { return _framecount; } + bool InRollback() { return _rollingback; } + + bool GetEvent(Event &e); + +protected: + friend SyncTestBackend; + + struct SavedFrame { + byte *buf; + int cbuf; + int frame; + int checksum; + SavedFrame() : buf(NULL), cbuf(0), frame(-1), checksum(0) { } + }; + struct SavedState { + SavedFrame frames[MAX_PREDICTION_FRAMES + 2]; + int head; + }; + + void LoadFrame(int frame); + void SaveCurrentFrame(); + int FindSavedFrameIndex(int frame); + SavedFrame &GetLastSavedFrame(); + + bool CreateQueues(Config &config); + bool CheckSimulationConsistency(int *seekTo); + void ResetPrediction(int frameNumber); + +protected: + GGPOSessionCallbacks _callbacks; + SavedState _savedstate; + Config _config; + + bool _rollingback; + int _last_confirmed_frame; + int _framecount; + int _max_prediction_frames; + + InputQueue *_input_queues; + + RingBuffer _event_queue; + UdpMsg::connect_status *_local_connect_status; +}; + +#endif + diff --git a/core/deps/ggpo/lib/ggpo/timesync.cpp b/core/deps/ggpo/lib/ggpo/timesync.cpp new file mode 100644 index 000000000..5fd7722f0 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/timesync.cpp @@ -0,0 +1,85 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#include "timesync.h" + +TimeSync::TimeSync() +{ + memset(_local, 0, sizeof(_local)); + memset(_remote, 0, sizeof(_remote)); + _next_prediction = FRAME_WINDOW_SIZE * 3; +} + +TimeSync::~TimeSync() +{ +} + +void +TimeSync::advance_frame(GameInput &input, int advantage, int radvantage) +{ + // Remember the last frame and frame advantage + _last_inputs[input.frame % ARRAY_SIZE(_last_inputs)] = input; + _local[input.frame % ARRAY_SIZE(_local)] = advantage; + _remote[input.frame % ARRAY_SIZE(_remote)] = radvantage; +} + +int +TimeSync::recommend_frame_wait_duration(bool require_idle_input) +{ + // Average our local and remote frame advantages + int sum = 0; + float advantage, radvantage; + for (size_t i = 0; i < ARRAY_SIZE(_local); i++) { + sum += _local[i]; + } + advantage = sum / (float)ARRAY_SIZE(_local); + + sum = 0; + for (size_t i = 0; i < ARRAY_SIZE(_remote); i++) { + sum += _remote[i]; + } + radvantage = sum / (float)ARRAY_SIZE(_remote); + + static int count = 0; + count++; + + // See if someone should take action. The person furthest ahead + // needs to slow down so the other user can catch up. + // Only do this if both clients agree on who's ahead!! + if (advantage >= radvantage) { + return 0; + } + + // Both clients agree that we're the one ahead. Split + // the difference between the two to figure out how long to + // sleep for. + int sleep_frames = (int)(((radvantage - advantage) / 2) + 0.5); + + Log("iteration %d: sleep frames is %d\n", count, sleep_frames); + + // Some things just aren't worth correcting for. Make sure + // the difference is relevant before proceeding. + if (sleep_frames < MIN_FRAME_ADVANTAGE) { + return 0; + } + + // Make sure our input had been "idle enough" before recommending + // a sleep. This tries to make the emulator sleep while the + // user's input isn't sweeping in arcs (e.g. fireball motions in + // Street Fighter), which could cause the player to miss moves. + if (require_idle_input) { + for (size_t i = 1; i < ARRAY_SIZE(_last_inputs); i++) { + if (!_last_inputs[i].equal(_last_inputs[0], true)) { + Log("iteration %d: rejecting due to input stuff at position %d...!!!\n", count, i); + return 0; + } + } + } + + // Success!!! Recommend the number of frames to sleep and adjust + return MIN(sleep_frames, MAX_FRAME_ADVANTAGE); +} diff --git a/core/deps/ggpo/lib/ggpo/timesync.h b/core/deps/ggpo/lib/ggpo/timesync.h new file mode 100644 index 000000000..83b66d731 --- /dev/null +++ b/core/deps/ggpo/lib/ggpo/timesync.h @@ -0,0 +1,34 @@ +/* ----------------------------------------------------------------------- + * GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC. + * + * Use of this software is governed by the MIT license that can be found + * in the LICENSE file. + */ + +#ifndef _TIMESYNC_H +#define _TIMESYNC_H + +#include "game_input.h" +#include "ggpo_types.h" + +#define FRAME_WINDOW_SIZE 40 +#define MIN_UNIQUE_FRAMES 10 +#define MIN_FRAME_ADVANTAGE 3 +#define MAX_FRAME_ADVANTAGE 9 + +class TimeSync { +public: + TimeSync(); + virtual ~TimeSync (); + + void advance_frame(GameInput &input, int advantage, int radvantage); + int recommend_frame_wait_duration(bool require_idle_input); + +protected: + int _local[FRAME_WINDOW_SIZE]; + int _remote[FRAME_WINDOW_SIZE]; + GameInput _last_inputs[MIN_UNIQUE_FRAMES]; + int _next_prediction; +}; + +#endif diff --git a/core/emulator.cpp b/core/emulator.cpp index 8705c8b65..4489d6572 100644 --- a/core/emulator.cpp +++ b/core/emulator.cpp @@ -35,6 +35,8 @@ #include "hw/pvr/Renderer_if.h" #include "rend/CustomTexture.h" #include "hw/arm7/arm7_rec.h" +#include "network/ggpo.h" +#include "hw/mem/mem_watch.h" extern int screen_width, screen_height; @@ -527,13 +529,23 @@ static void *dc_run_thread(void*) InitAudio(); try { - dc_run(); + memwatch::protect(); + while (true) + { + dc_run(); + if (settings.endOfFrame) + settings.endOfFrame = false; + else + break; + ggpo::nextFrame(); + } } catch (const FlycastException& e) { ERROR_LOG(COMMON, "%s", e.what()); sh4_cpu.Stop(); lastError = e.what(); } + ggpo::stopSession(); TermAudio(); return nullptr; @@ -589,6 +601,7 @@ void dc_term_game() config::Settings::instance().reset(); config::Settings::instance().load(false); + ggpo::stopSession(); } void dc_term_emulator() diff --git a/core/hw/aica/sgc_if.cpp b/core/hw/aica/sgc_if.cpp index 5376bd34c..8a50aaca1 100755 --- a/core/hw/aica/sgc_if.cpp +++ b/core/hw/aica/sgc_if.cpp @@ -423,7 +423,7 @@ struct ChannelEx void (* plfo_calc)(ChannelEx* ch); __forceinline void Step(ChannelEx* ch) { counter--;if (counter==0) { state++; counter=start_value; alfo_calc(ch);plfo_calc(ch); } } void Reset(ChannelEx* ch) { state=0; counter=start_value; alfo_calc(ch); plfo_calc(ch); } - void SetStartValue(u32 nv) { start_value=nv;counter=start_value; } + void SetStartValue(u32 nv) { start_value = nv;} } lfo; bool enabled; //set to false to 'freeze' the channel @@ -679,7 +679,7 @@ struct ChannelEx } //LFORE,LFOF,PLFOWS,PLFOS,ALFOWS,ALFOS - void UpdateLFO() + void UpdateLFO(bool derivedState) { { int N=ccd->LFOF; @@ -689,6 +689,8 @@ struct ChannelEx int L = (G-1)<<2; int O = L + G * (M+1); lfo.SetStartValue(O); + if (!derivedState) + lfo.counter = O; } lfo.alfo_shft=8-ccd->ALFOS; @@ -697,7 +699,7 @@ struct ChannelEx lfo.plfo_calc=PLFOWS_CALC[ccd->PLFOWS]; lfo.plfo_scale = PLFO_Scales[ccd->PLFOS]; - if (ccd->LFORE) + if (ccd->LFORE && !derivedState) { lfo.Reset(this); } @@ -809,7 +811,7 @@ struct ChannelEx case 0x1C://ALFOS,ALFOWS,PLFOS case 0x1D://PLFOWS,LFOF,LFORE - UpdateLFO(); + UpdateLFO(false); break; case 0x20://ISEL,IMXL @@ -1454,7 +1456,7 @@ void AICA_Sample32() clip16(mixl); clip16(mixr); - if (!settings.input.fastForwardMode && !config::DisableSound) + if (!settings.input.fastForwardMode && !settings.aica.muteAudio) WriteSample(mixr,mixl); } } @@ -1498,7 +1500,7 @@ void AICA_Sample() VOLPAN(*(s16*)&DSPData->EFREG[i], dsp_out_vol[i].EFSDL, dsp_out_vol[i].EFPAN, mixl, mixr); } - if (settings.input.fastForwardMode || config::DisableSound) + if (settings.input.fastForwardMode || settings.aica.muteAudio) return; //Mono ! @@ -1673,7 +1675,7 @@ bool channel_unserialize(void **data, unsigned int *total_size, serialize_versio REICAST_US(dumu8); // Chans[i].lfo.alfo_calc_lut REICAST_US(dumu8); // Chans[i].lfo.plfo_calc_lut } - Chans[i].UpdateLFO(); + Chans[i].UpdateLFO(true); REICAST_US(Chans[i].enabled) ; if (old_format) REICAST_US(dum); // Chans[i].ChannelNumber diff --git a/core/hw/arm7/arm7.cpp b/core/hw/arm7/arm7.cpp index 1f0709fc7..58c65e090 100644 --- a/core/hw/arm7/arm7.cpp +++ b/core/hw/arm7/arm7.cpp @@ -48,32 +48,34 @@ static void CPUUpdateFlags(); static void CPUSoftwareInterrupt(int comment); static void CPUUndefinedException(); -#if FEAT_AREC == DYNAREC_NONE - // // ARM7 interpreter // -static int clockTicks; +int arm7ClockTicks; + +#if FEAT_AREC == DYNAREC_NONE static void runInterpreter(u32 CycleCount) { if (!Arm7Enabled) return; - clockTicks -= CycleCount; - while (clockTicks < 0) + arm7ClockTicks -= CycleCount; + while (arm7ClockTicks < 0) { if (reg[INTR_PEND].I) CPUFiq(); reg[15].I = armNextPC + 8; + + int& clockTicks = arm7ClockTicks; #include "arm-new.h" } } void aicaarm::avoidRaceCondition() { - clockTicks = std::min(clockTicks, -50); + arm7ClockTicks = std::min(arm7ClockTicks, -50); } void aicaarm::run(u32 samples) diff --git a/core/hw/arm7/arm7.h b/core/hw/arm7/arm7.h index f0cf597d0..4c1df3683 100644 --- a/core/hw/arm7/arm7.h +++ b/core/hw/arm7/arm7.h @@ -99,6 +99,7 @@ typedef union alignas(8) extern reg_pair arm_Reg[RN_ARM_REG_COUNT]; #define ARM_CYCLES_PER_SAMPLE 256 +extern int arm7ClockTicks; void CPUFiq(); void CPUUpdateCPSR(); diff --git a/core/hw/maple/maple_cfg.cpp b/core/hw/maple/maple_cfg.cpp index 57cf2fe62..2449cfc53 100644 --- a/core/hw/maple/maple_cfg.cpp +++ b/core/hw/maple/maple_cfg.cpp @@ -5,6 +5,8 @@ #include "input/gamepad_device.h" #include "cfg/option.h" +u32 maple_kcode[4]; + static u8 GetBtFromSgn(s8 val) { return val+128; @@ -66,7 +68,7 @@ void MapleConfigMap::GetInput(PlainJoystickState* pjs) if (settings.platform.system == DC_PLATFORM_DREAMCAST) { - pjs->kcode = kcode[player_num]; + pjs->kcode = maple_kcode[player_num]; pjs->joy[PJAI_X1] = GetBtFromSgn(joyx[player_num]); pjs->joy[PJAI_Y1] = GetBtFromSgn(joyy[player_num]); pjs->trigger[PJTI_R] = rt[player_num]; @@ -75,13 +77,13 @@ void MapleConfigMap::GetInput(PlainJoystickState* pjs) else if (settings.platform.system == DC_PLATFORM_ATOMISWAVE) { #ifdef LIBRETRO - pjs->kcode = kcode[player_num]; + pjs->kcode = maple_kcode[player_num]; #else const u32* mapping = settings.input.JammaSetup == JVS::LightGun ? awavelg_button_mapping : awave_button_mapping; pjs->kcode = ~0; for (u32 i = 0; i < ARRAY_SIZE(awave_button_mapping); i++) { - if ((kcode[player_num] & (1 << i)) == 0) + if ((maple_kcode[player_num] & (1 << i)) == 0) pjs->kcode &= ~mapping[i]; } #endif @@ -171,11 +173,11 @@ void MapleConfigMap::GetMouseInput(u8& buttons, int& x, int& y, int& wheel) bool maple_atomiswave_coin_chute(int slot) { #ifdef LIBRETRO - return kcode[slot] & AWAVE_COIN_KEY; + return maple_kcode[slot] & AWAVE_COIN_KEY; #else for (int i = 0; i < 16; i++) { - if ((kcode[slot] & (1 << i)) == 0 && awave_button_mapping[i] == AWAVE_COIN_KEY) + if ((maple_kcode[slot] & (1 << i)) == 0 && awave_button_mapping[i] == AWAVE_COIN_KEY) return true; } return false; diff --git a/core/hw/maple/maple_cfg.h b/core/hw/maple/maple_cfg.h index 6d2b2b829..24a06f94b 100644 --- a/core/hw/maple/maple_cfg.h +++ b/core/hw/maple/maple_cfg.h @@ -70,6 +70,8 @@ private: maple_device* dev; }; +extern u32 maple_kcode[4]; + void mcfg_CreateDevices(); void mcfg_CreateNAOMIJamma(); void mcfg_CreateAtomisWaveControllers(); diff --git a/core/hw/maple/maple_if.cpp b/core/hw/maple/maple_if.cpp index 4d078a20e..be7cc06af 100644 --- a/core/hw/maple/maple_if.cpp +++ b/core/hw/maple/maple_if.cpp @@ -5,6 +5,7 @@ #include "hw/holly/sb.h" #include "hw/sh4/sh4_mem.h" #include "hw/sh4/sh4_sched.h" +#include "network/ggpo.h" enum MaplePattern { @@ -19,7 +20,6 @@ maple_device* MapleDevices[MAPLE_PORTS][6]; int maple_schid; -void UpdateInputState(); /* Maple host controller Direct processing, async interrupt handling @@ -147,7 +147,7 @@ static void maple_DoDma() } #endif - UpdateInputState(); + ggpo::getInput(maple_kcode); const bool swap_msb = (SB_MMSEL == 0); u32 xfer_count=0; diff --git a/core/hw/maple/maple_jvs.cpp b/core/hw/maple/maple_jvs.cpp index 7abd4b056..07f6318aa 100644 --- a/core/hw/maple/maple_jvs.cpp +++ b/core/hw/maple/maple_jvs.cpp @@ -1417,11 +1417,11 @@ u32 jvs_io_board::handle_jvs_message(u8 *buffer_in, u32 length_in, u8 *buffer_ou u32 buttons[4] {}; #ifdef LIBRETRO for (int p = 0; p < 4; p++) - buttons[p] = ~kcode[p]; + buttons[p] = ~maple_kcode[p]; #else for (u32 i = 0; i < ARRAY_SIZE(naomi_button_mapping); i++) for (int p = 0; p < 4; p++) - if ((kcode[p] & (1 << i)) == 0) + if ((maple_kcode[p] & (1 << i)) == 0) buttons[p] |= naomi_button_mapping[i]; #endif diff --git a/core/hw/mem/_vmem.h b/core/hw/mem/_vmem.h index e4ab503fa..184e1d2eb 100644 --- a/core/hw/mem/_vmem.h +++ b/core/hw/mem/_vmem.h @@ -115,3 +115,5 @@ void _vmem_bm_reset(); void _vmem_protect_vram(u32 addr, u32 size); void _vmem_unprotect_vram(u32 addr, u32 size); u32 _vmem_get_vram_offset(void *addr); +bool BM_LockedWrite(u8* address); + diff --git a/core/hw/mem/mem_watch.cpp b/core/hw/mem/mem_watch.cpp new file mode 100644 index 000000000..bbc47f5a9 --- /dev/null +++ b/core/hw/mem/mem_watch.cpp @@ -0,0 +1,111 @@ +/* + Copyright 2021 flyinghead + + This file is part of Flycast. + + Flycast 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 Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#include "mem_watch.h" + +namespace memwatch +{ + +VramWatcher vramWatcher; +RamWatcher ramWatcher; +AicaRamWatcher aramWatcher; + +void AicaRamWatcher::protectMem(u32 addr, u32 size) +{ + size = std::min(ARAM_SIZE - addr, size) & ~PAGE_MASK; + if (_nvmem_enabled() && _nvmem_4gb_space()) { + mem_region_lock(virt_ram_base + 0x00800000 + addr, size); // P0 + mem_region_lock(virt_ram_base + 0x02800000 + addr, size);// P0 - mirror + mem_region_lock(virt_ram_base + 0x80800000 + addr, size); // P1 + //mem_region_lock(virt_ram_base + 0x82800000 + addr, size); // P1 - mirror + mem_region_lock(virt_ram_base + 0xA0800000 + addr, size); // P2 + //mem_region_lock(virt_ram_base + 0xA2800000 + addr, size); // P2 - mirror + if (ARAM_SIZE == 2 * 1024 * 1024) { + mem_region_lock(virt_ram_base + 0x00A00000 + addr, size); // P0 + mem_region_lock(virt_ram_base + 0x00C00000 + addr, size); // P0 + mem_region_lock(virt_ram_base + 0x00E00000 + addr, size); // P0 + mem_region_lock(virt_ram_base + 0x02A00000 + addr, size);// P0 - mirror + mem_region_lock(virt_ram_base + 0x02C00000 + addr, size);// P0 - mirror + mem_region_lock(virt_ram_base + 0x02E00000 + addr, size);// P0 - mirror + mem_region_lock(virt_ram_base + 0x80A00000 + addr, size); // P1 + mem_region_lock(virt_ram_base + 0x80C00000 + addr, size); // P1 + mem_region_lock(virt_ram_base + 0x80E00000 + addr, size); // P1 + mem_region_lock(virt_ram_base + 0xA0A00000 + addr, size); // P2 + mem_region_lock(virt_ram_base + 0xA0C00000 + addr, size); // P2 + mem_region_lock(virt_ram_base + 0xA0E00000 + addr, size); // P2 + } + } else { + mem_region_lock(aica_ram.data + addr, + std::min(aica_ram.size - addr, size)); + } +} + +void AicaRamWatcher::unprotectMem(u32 addr, u32 size) +{ + size = std::min(ARAM_SIZE - addr, size) & ~PAGE_MASK; + if (_nvmem_enabled() && _nvmem_4gb_space()) { + mem_region_unlock(virt_ram_base + 0x00800000 + addr, size); // P0 + mem_region_unlock(virt_ram_base + 0x02800000 + addr, size); // P0 - mirror + mem_region_unlock(virt_ram_base + 0x80800000 + addr, size); // P1 + //mem_region_unlock(virt_ram_base + 0x82800000 + addr, size); // P1 - mirror + mem_region_unlock(virt_ram_base + 0xA0800000 + addr, size); // P2 + //mem_region_unlock(virt_ram_base + 0xA2800000 + addr, size); // P2 - mirror + if (ARAM_SIZE == 2 * 1024 * 1024) { + mem_region_unlock(virt_ram_base + 0x00A00000 + addr, size); // P0 + mem_region_unlock(virt_ram_base + 0x00C00000 + addr, size); // P0 + mem_region_unlock(virt_ram_base + 0x00E00000 + addr, size); // P0 + mem_region_unlock(virt_ram_base + 0x02A00000 + addr, size); // P0 - mirror + mem_region_unlock(virt_ram_base + 0x02C00000 + addr, size); // P0 - mirror + mem_region_unlock(virt_ram_base + 0x02E00000 + addr, size); // P0 - mirror + mem_region_unlock(virt_ram_base + 0x80A00000 + addr, size); // P1 + mem_region_unlock(virt_ram_base + 0x80C00000 + addr, size); // P1 + mem_region_unlock(virt_ram_base + 0x80E00000 + addr, size); // P1 + mem_region_unlock(virt_ram_base + 0xA0A00000 + addr, size); // P2 + mem_region_unlock(virt_ram_base + 0xA0C00000 + addr, size); // P2 + mem_region_unlock(virt_ram_base + 0xA0E00000 + addr, size); // P2 + } + } else { + mem_region_unlock(aica_ram.data + addr, + std::min(aica_ram.size - addr, size)); + } +} + +u32 AicaRamWatcher::getMemOffset(void *p) +{ + u32 addr; + if (_nvmem_enabled() && _nvmem_4gb_space()) { + if ((u8*) p < virt_ram_base || (u8*) p >= virt_ram_base + 0x100000000L) + return -1; + addr = (u32) ((u8*) p - virt_ram_base); + u32 area = (addr >> 29) & 7; + if (area != 0 && area != 4 && area != 5) + return -1; + addr &= 0x1fffffff & ~0x02000000; + if (addr < 0x00800000 || addr >= 0x01000000) + return -1; + addr &= ARAM_MASK; + } else { + if ((u8*) p < &aica_ram[0] || (u8*) p >= &aica_ram[ARAM_SIZE]) + return -1; + addr = (u32) ((u8*) p - &aica_ram[0]); + } + return addr; +} + +} + diff --git a/core/hw/mem/mem_watch.h b/core/hw/mem/mem_watch.h new file mode 100644 index 000000000..c96e11129 --- /dev/null +++ b/core/hw/mem/mem_watch.h @@ -0,0 +1,184 @@ +/* + Copyright 2021 flyinghead + + This file is part of Flycast. + + Flycast 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 Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#include "types.h" +#include "_vmem.h" +#include "hw/aica/aica_if.h" +#include "hw/sh4/dyna/blockmanager.h" +#include "hw/sh4/sh4_mem.h" +#include "hw/pvr/pvr_mem.h" +#include "rend/TexCache.h" +#include +#include + +namespace memwatch +{ + +using PageMap = std::unordered_map>; + +template +class Watcher +{ + bool started; + PageMap pages; + +public: + void protect() + { + if (!started) + { + static_cast(*this).protectMem(0, 0xffffffff); + started = true; + } + else + { + for (const auto& pair : pages) + static_cast(*this).protectMem(pair.first, PAGE_SIZE); + } + pages.clear(); + } + + void reset() + { + started = false; + pages.clear(); + } + + bool hit(void *addr) + { + u32 offset = static_cast(*this).getMemOffset(addr); + if (offset == (u32)-1) + return false; + offset &= ~PAGE_MASK; + if (pages.count(offset) > 0) + // already saved + return true; + memcpy(&pages[offset][0], static_cast(*this).getMemPage(offset), PAGE_SIZE); + static_cast(*this).unprotectMem(offset, PAGE_SIZE); + return true; + } + + const PageMap& getPages() { + return pages; + } +}; + +class VramWatcher : public Watcher +{ + friend class Watcher; + +protected: + void protectMem(u32 addr, u32 size) + { + _vmem_protect_vram(addr, std::min(VRAM_SIZE - addr, size) & ~PAGE_MASK); + } + + void unprotectMem(u32 addr, u32 size) + { + _vmem_unprotect_vram(addr, std::min(VRAM_SIZE - addr, size) & ~PAGE_MASK); + } + + u32 getMemOffset(void *p) + { + return _vmem_get_vram_offset(p); + } + +public: + void *getMemPage(u32 addr) + { + return &vram[addr]; + } +}; + +class RamWatcher : public Watcher +{ + friend class Watcher; + +protected: + void protectMem(u32 addr, u32 size) + { + bm_LockPage(addr, std::min(VRAM_SIZE - addr, size) & ~PAGE_MASK); + } + + void unprotectMem(u32 addr, u32 size) + { + bm_UnlockPage(addr, std::min(VRAM_SIZE - addr, size) & ~PAGE_MASK); + } + + u32 getMemOffset(void *p) + { + return bm_getRamOffset(p); + } + +public: + void *getMemPage(u32 addr) + { + return &mem_b[addr]; + } +}; + +class AicaRamWatcher : public Watcher +{ + friend class Watcher; + +protected: + void protectMem(u32 addr, u32 size); + void unprotectMem(u32 addr, u32 size); + u32 getMemOffset(void *p); + +public: + void *getMemPage(u32 addr) + { + return &aica_ram[addr]; + } +}; + +extern VramWatcher vramWatcher; +extern RamWatcher ramWatcher; +extern AicaRamWatcher aramWatcher; + +inline static bool writeAccess(void *p) +{ + if (vramWatcher.hit(p)) + { + VramLockedWrite((u8 *)p); + return true; + } + if (ramWatcher.hit(p)) + { + bm_RamWriteAccess(p); + return true; + } + return aramWatcher.hit(p); +} + +inline static void protect() +{ + vramWatcher.protect(); + ramWatcher.protect(); + aramWatcher.protect(); +} + +inline static void reset() +{ + vramWatcher.reset(); + ramWatcher.reset(); + aramWatcher.reset(); +} + +} diff --git a/core/hw/pvr/Renderer_if.cpp b/core/hw/pvr/Renderer_if.cpp index 22e077cbd..af4196da8 100644 --- a/core/hw/pvr/Renderer_if.cpp +++ b/core/hw/pvr/Renderer_if.cpp @@ -5,6 +5,7 @@ #include "hw/pvr/pvr_mem.h" #include "rend/TexCache.h" #include "cfg/option.h" +#include "network/ggpo.h" #include #include @@ -386,6 +387,11 @@ void rend_start_render() else rs.Set(); } + if (ggpo::active() && !config::DelayFrameSwapping) + { + settings.endOfFrame = true; + sh4_cpu.Stop(); + } } } @@ -456,6 +462,11 @@ void rend_swap_frame(u32 fb_r_sof1) rend_single_frame(true); swap_mutex.lock(); } + if (ggpo::active() && config::DelayFrameSwapping) + { + settings.endOfFrame = true; + sh4_cpu.Stop(); + } } swap_mutex.unlock(); } diff --git a/core/hw/pvr/ta_ctx.cpp b/core/hw/pvr/ta_ctx.cpp index 1b8865e89..15d7d3e45 100644 --- a/core/hw/pvr/ta_ctx.cpp +++ b/core/hw/pvr/ta_ctx.cpp @@ -80,21 +80,25 @@ bool QueueRender(TA_context* ctx) { verify(ctx != 0); - bool skipFrame = false; - RenderCount++; - if (RenderCount % (config::SkipFrame + 1) != 0) - skipFrame = true; - else if (config::ThreadedRendering && rqueue != nullptr - && (config::AutoSkipFrame == 0 || (config::AutoSkipFrame == 1 && SH4FastEnough))) - // The previous render hasn't completed yet so we wait. - // If autoskipframe is enabled (normal level), we only do so if the CPU is running - // fast enough over the last frames - frame_finished.Wait(); + bool skipFrame = settings.disableRenderer; + if (!skipFrame) + { + RenderCount++; + if (RenderCount % (config::SkipFrame + 1) != 0) + skipFrame = true; + else if (config::ThreadedRendering && rqueue != nullptr + && (config::AutoSkipFrame == 0 || (config::AutoSkipFrame == 1 && SH4FastEnough))) + // The previous render hasn't completed yet so we wait. + // If autoskipframe is enabled (normal level), we only do so if the CPU is running + // fast enough over the last frames + frame_finished.Wait(); + } if (skipFrame || rqueue) { tactx_Recycle(ctx); - fskip++; + if (!settings.disableRenderer) + fskip++; return false; } diff --git a/core/hw/sh4/dyna/blockmanager.cpp b/core/hw/sh4/dyna/blockmanager.cpp index 8e37eb556..e9f3e3b02 100644 --- a/core/hw/sh4/dyna/blockmanager.cpp +++ b/core/hw/sh4/dyna/blockmanager.cpp @@ -260,41 +260,41 @@ void bm_Reset() } } -static void bm_LockPage(u32 addr) +void bm_LockPage(u32 addr, u32 size) { addr = addr & (RAM_MASK - PAGE_MASK); if (_nvmem_enabled()) { - mem_region_lock(virt_ram_base + 0x0C000000 + addr, PAGE_SIZE); + mem_region_lock(virt_ram_base + 0x0C000000 + addr, size); if (_nvmem_4gb_space()) { - mem_region_lock(virt_ram_base + 0x8C000000 + addr, PAGE_SIZE); - mem_region_lock(virt_ram_base + 0xAC000000 + addr, PAGE_SIZE); + mem_region_lock(virt_ram_base + 0x8C000000 + addr, size); + mem_region_lock(virt_ram_base + 0xAC000000 + addr, size); // TODO wraps } } else { - mem_region_lock(&mem_b[addr], PAGE_SIZE); + mem_region_lock(&mem_b[addr], size); } } -static void bm_UnlockPage(u32 addr) +void bm_UnlockPage(u32 addr, u32 size) { addr = addr & (RAM_MASK - PAGE_MASK); if (_nvmem_enabled()) { - mem_region_unlock(virt_ram_base + 0x0C000000 + addr, PAGE_SIZE); + mem_region_unlock(virt_ram_base + 0x0C000000 + addr, size); if (_nvmem_4gb_space()) { - mem_region_unlock(virt_ram_base + 0x8C000000 + addr, PAGE_SIZE); - mem_region_unlock(virt_ram_base + 0xAC000000 + addr, PAGE_SIZE); + mem_region_unlock(virt_ram_base + 0x8C000000 + addr, size); + mem_region_unlock(virt_ram_base + 0xAC000000 + addr, size); // TODO wraps } } else { - mem_region_unlock(&mem_b[addr], PAGE_SIZE); + mem_region_unlock(&mem_b[addr], size); } } @@ -589,49 +589,60 @@ void bm_RamWriteAccess(u32 addr) addr &= RAM_MASK; if (unprotected_pages[addr / PAGE_SIZE]) { - ERROR_LOG(DYNAREC, "Page %08x already unprotected", addr); - die("Fatal error"); + //ERROR_LOG(DYNAREC, "Page %08x already unprotected", addr); + //die("Fatal error"); + return; } unprotected_pages[addr / PAGE_SIZE] = true; bm_UnlockPage(addr); std::set& block_list = blocks_per_page[addr / PAGE_SIZE]; - std::vector list_copy; - list_copy.insert(list_copy.begin(), block_list.begin(), block_list.end()); - if (!list_copy.empty()) - DEBUG_LOG(DYNAREC, "bm_RamWriteAccess write access to %08x pc %08x", addr, next_pc); - for (auto& block : list_copy) + if (!block_list.empty()) { - bm_DiscardBlock(block); + std::vector list_copy; + list_copy.insert(list_copy.begin(), block_list.begin(), block_list.end()); + if (!list_copy.empty()) + DEBUG_LOG(DYNAREC, "bm_RamWriteAccess write access to %08x pc %08x", addr, next_pc); + for (auto& block : list_copy) + { + bm_DiscardBlock(block); + } + verify(block_list.empty()); } - verify(block_list.empty()); } -bool bm_RamWriteAccess(void *p) +u32 bm_getRamOffset(void *p) { if (_nvmem_enabled()) { if (_nvmem_4gb_space()) { if ((u8 *)p < virt_ram_base || (u8 *)p >= virt_ram_base + 0x100000000L) - return false; + return -1; } else { if ((u8 *)p < virt_ram_base || (u8 *)p >= virt_ram_base + 0x20000000) - return false; + return -1; } u32 addr = (u8*)p - virt_ram_base; if (!IsOnRam(addr) || ((addr >> 29) > 0 && (addr >> 29) < 4)) // system RAM is not mapped to 20, 40 and 60 because of laziness - return false; - bm_RamWriteAccess(addr); + return -1; + return addr & RAM_MASK; } else { if ((u8 *)p < &mem_b[0] || (u8 *)p >= &mem_b[RAM_SIZE]) - return false; - bm_RamWriteAccess((u32)((u8 *)p - &mem_b[0])); + return -1; + return (u32)((u8 *)p - &mem_b[0]); } +} +bool bm_RamWriteAccess(void *p) +{ + u32 offset = bm_getRamOffset(p); + if (offset == (u32)-1) + return false; + bm_RamWriteAccess(offset); return true; } diff --git a/core/hw/sh4/dyna/blockmanager.h b/core/hw/sh4/dyna/blockmanager.h index be6479274..83ad98a48 100644 --- a/core/hw/sh4/dyna/blockmanager.h +++ b/core/hw/sh4/dyna/blockmanager.h @@ -101,3 +101,7 @@ static inline bool bm_IsRamPageProtected(u32 addr) addr &= RAM_MASK; return !unprotected_pages[addr / PAGE_SIZE]; } +void bm_LockPage(u32 addr, u32 size = PAGE_SIZE); +void bm_UnlockPage(u32 addr, u32 size = PAGE_SIZE); +u32 bm_getRamOffset(void *p); + diff --git a/core/hw/sh4/interpr/sh4_interpreter.cpp b/core/hw/sh4/interpr/sh4_interpreter.cpp index 66254a7ed..2249afa94 100644 --- a/core/hw/sh4/interpr/sh4_interpreter.cpp +++ b/core/hw/sh4/interpr/sh4_interpreter.cpp @@ -19,14 +19,14 @@ sh4_icache icache; sh4_ocache ocache; -static s32 l; +s32 sh4InterpCycles; static void ExecuteOpcode(u16 op) { if (sr.FD == 1 && OpDesc[op]->IsFloatingPoint()) RaiseFPUDisableException(); OpPtr[op](op); - l -= CPU_RATIO; + sh4InterpCycles -= CPU_RATIO; } static u16 ReadNexOp() @@ -42,7 +42,7 @@ static void Sh4_int_Run() sh4_int_bCpuRun = true; RestoreHostRoundingMode(); - l += SH4_TIMESLICE; + sh4InterpCycles += SH4_TIMESLICE; try { do @@ -53,12 +53,12 @@ static void Sh4_int_Run() u32 op = ReadNexOp(); ExecuteOpcode(op); - } while (l > 0); - l += SH4_TIMESLICE; + } while (sh4InterpCycles > 0); + sh4InterpCycles += SH4_TIMESLICE; UpdateSystem_INTC(); } catch (const SH4ThrownException& ex) { Do_Exception(ex.epc, ex.expEvn, ex.callVect); - l -= CPU_RATIO * 5; // an exception requires the instruction pipeline to drain, so approx 5 cycles + sh4InterpCycles -= CPU_RATIO * 5; // an exception requires the instruction pipeline to drain, so approx 5 cycles } } while (sh4_int_bCpuRun); } catch (const debugger::Stop& e) { @@ -82,7 +82,7 @@ static void Sh4_int_Step() ExecuteOpcode(op); } catch (const SH4ThrownException& ex) { Do_Exception(ex.epc, ex.expEvn, ex.callVect); - l -= CPU_RATIO * 5; // an exception requires the instruction pipeline to drain, so approx 5 cycles + sh4InterpCycles -= CPU_RATIO * 5; // an exception requires the instruction pipeline to drain, so approx 5 cycles } catch (const debugger::Stop& e) { } } @@ -110,6 +110,7 @@ static void Sh4_int_Reset(bool hard) UpdateFPSCR(); icache.Reset(hard); ocache.Reset(hard); + sh4InterpCycles = 0; INFO_LOG(INTERPRETER, "Sh4 Reset"); } diff --git a/core/hw/sh4/sh4_interpreter.h b/core/hw/sh4/sh4_interpreter.h index aa3e5104f..85b31f46e 100644 --- a/core/hw/sh4/sh4_interpreter.h +++ b/core/hw/sh4/sh4_interpreter.h @@ -45,3 +45,5 @@ void ExecuteDelayslot_RTE(); int UpdateSystem(); int UpdateSystem_INTC(); + +extern s32 sh4InterpCycles; diff --git a/core/linux/common.cpp b/core/linux/common.cpp index 04033b400..5927b4719 100644 --- a/core/linux/common.cpp +++ b/core/linux/common.cpp @@ -18,6 +18,9 @@ #include "oslib/host_context.h" #include "hw/sh4/dyna/ngen.h" +#include "rend/TexCache.h" +#include "hw/mem/_vmem.h" +#include "hw/mem/mem_watch.h" #ifdef __SWITCH__ #include @@ -26,8 +29,6 @@ extern "C" char __start__; #endif // __SWITCH__ #if !defined(TARGET_NO_EXCEPTIONS) -bool VramLockedWrite(u8* address); -bool BM_LockedWrite(u8* address); void context_from_segfault(host_context_t* hctx, void* segfault_ctx); void context_to_segfault(host_context_t* hctx, void* segfault_ctx); @@ -41,6 +42,9 @@ static struct sigaction next_bus_handler; void fault_handler(int sn, siginfo_t * si, void *segfault_ctx) { + // Ram watcher for net rollback + if (memwatch::writeAccess(si->si_addr)) + return; // code protection in RAM if (bm_RamWriteAccess(si->si_addr)) return; diff --git a/core/network/ggpo.cpp b/core/network/ggpo.cpp new file mode 100644 index 000000000..ab34411c5 --- /dev/null +++ b/core/network/ggpo.cpp @@ -0,0 +1,500 @@ +/* + Copyright 2021 flyinghead + + This file is part of Flycast. + + Flycast 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 Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#ifndef LIBRETRO +#include "ggponet.h" +#include "ggpo.h" +#include "input/gamepad_device.h" +#include "emulator.h" +#include "rend/gui.h" +#include "hw/mem/mem_watch.h" +#include "hw/sh4/sh4_sched.h" +#include +#include +#include +#include +#include +#include + +//#define SYNC_TEST 1 + +namespace ggpo +{ + +constexpr int FRAME_DELAY = 2; +static GGPOSession *ggpoSession; +static int localPlayerNum; +static GGPOPlayerHandle localPlayer; +static GGPOPlayerHandle remotePlayer; +static bool synchronized; +static std::mutex ggpoMutex; + +struct MemPages +{ + void load() + { + ram = memwatch::ramWatcher.getPages(); + vram = memwatch::vramWatcher.getPages(); + aram = memwatch::aramWatcher.getPages(); + } + memwatch::PageMap ram; + memwatch::PageMap vram; + memwatch::PageMap aram; +}; +static std::unordered_map deltaStates; +static int lastSavedFrame = -1; + +/* + * begin_game callback - This callback has been deprecated. You must + * implement it, but should ignore the 'game' parameter. + */ +static bool begin_game(const char *) +{ + DEBUG_LOG(NETWORK, "Game begin"); + return true; +} + +/* + * on_event - Notification that something has happened. See the GGPOEventCode + * structure for more information. + */ +static bool on_event(GGPOEvent *info) +{ + switch (info->code) { + case GGPO_EVENTCODE_CONNECTED_TO_PEER: + INFO_LOG(NETWORK, "Connected to peer %d", info->u.connected.player); + gui_display_notification("Connected to peer", 2000); + break; + case GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER: + INFO_LOG(NETWORK, "Synchronizing with peer %d", info->u.synchronizing.player); + gui_display_notification("Synchronizing with peer", 2000); + break; + case GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER: + INFO_LOG(NETWORK, "Synchronized with peer %d", info->u.synchronized.player); + gui_display_notification("Synchronized with peer", 2000); + break; + case GGPO_EVENTCODE_RUNNING: + INFO_LOG(NETWORK, "Running"); + gui_display_notification("Running", 2000); + synchronized = true; + break; + case GGPO_EVENTCODE_DISCONNECTED_FROM_PEER: + INFO_LOG(NETWORK, "Disconnected from peer %d", info->u.disconnected.player); + throw FlycastException("Disconnected from peer"); + break; + case GGPO_EVENTCODE_TIMESYNC: + INFO_LOG(NETWORK, "Timesync: %d frames ahead", info->u.timesync.frames_ahead); + std::this_thread::sleep_for(std::chrono::milliseconds(1000 * info->u.timesync.frames_ahead / 60)); // FIXME assumes 60 FPS + break; + case GGPO_EVENTCODE_CONNECTION_INTERRUPTED: + INFO_LOG(NETWORK, "Connection interrupted with player %d", info->u.connection_interrupted.player); + gui_display_notification("Connection interrupted", 2000); + break; + case GGPO_EVENTCODE_CONNECTION_RESUMED: + INFO_LOG(NETWORK, "Connection resumed with player %d", info->u.connection_resumed.player); + gui_display_notification("Connection resumed", 2000); + break; + } + return true; +} + +/* + * advance_frame - Called during a rollback. You should advance your game + * state by exactly one frame. Before each frame, call ggpo_synchronize_input + * to retrieve the inputs you should use for that frame. After each frame, + * you should call ggpo_advance_frame to notify GGPO.net that you're + * finished. + * + * The flags parameter is reserved. It can safely be ignored at this time. + */ +static bool advance_frame(int) +{ + INFO_LOG(NETWORK, "advance_frame"); + settings.aica.muteAudio = true; + settings.disableRenderer = true; + + dc_run(); + ggpo_advance_frame(ggpoSession); + + settings.aica.muteAudio = false; + settings.disableRenderer = false; + settings.endOfFrame = false; + + return true; +} + +/* + * load_game_state - GGPO.net will call this function at the beginning + * of a rollback. The buffer and len parameters contain a previously + * saved state returned from the save_game_state function. The client + * should make the current game state match the state contained in the + * buffer. + */ +static bool load_game_state(unsigned char *buffer, int len) +{ + INFO_LOG(NETWORK, "load_game_state"); + + // FIXME will invalidate too much stuff: palette/fog textures, maple stuff + // FIXME dynarecs + int frame = *(u32 *)buffer; + unsigned usedLen = sizeof(frame); + buffer += usedLen; + dc_unserialize((void **)&buffer, &usedLen, true); + if (len != (int)usedLen) + { + ERROR_LOG(NETWORK, "load_game_state len %d used %d", len, usedLen); + die("fatal"); + } + for (int f = lastSavedFrame - 1; f >= frame; f--) + { + const MemPages& pages = deltaStates[f]; + for (const auto& pair : pages.ram) + memcpy(memwatch::ramWatcher.getMemPage(pair.first), &pair.second[0], PAGE_SIZE); + for (const auto& pair : pages.vram) + memcpy(memwatch::vramWatcher.getMemPage(pair.first), &pair.second[0], PAGE_SIZE); + for (const auto& pair : pages.aram) + memcpy(memwatch::aramWatcher.getMemPage(pair.first), &pair.second[0], PAGE_SIZE); + DEBUG_LOG(NETWORK, "Restored frame %d pages: %d ram, %d vram, %d aica ram", f, (u32)pages.ram.size(), + (u32)pages.vram.size(), (u32)pages.aram.size()); + } + memwatch::reset(); + memwatch::protect(); + return true; +} + +/* + * save_game_state - The client should allocate a buffer, copy the + * entire contents of the current game state into it, and copy the + * length into the *len parameter. Optionally, the client can compute + * a checksum of the data and store it in the *checksum argument. + */ +static bool save_game_state(unsigned char **buffer, int *len, int *checksum, int frame) +{ + verify(!dc_is_running()); + lastSavedFrame = frame; + size_t allocSize = (settings.platform.system == DC_PLATFORM_NAOMI ? 20 : 10) * 1024 * 1024; + *buffer = (unsigned char *)malloc(allocSize); + if (*buffer == nullptr) + { + WARN_LOG(NETWORK, "Memory alloc failed"); + *len = 0; + return false; + } + u8 *data = *buffer; + *(u32 *)data = frame; + unsigned usedSize = sizeof(frame); + data += usedSize; + dc_serialize((void **)&data, &usedSize, true); + verify(usedSize < allocSize); + *len = usedSize; +#ifdef SYNC_TEST + *checksum = XXH32(*buffer, usedSize, 7); +#endif + if (frame > 0) + { + // Save the delta to frame-1 + if (deltaStates.count(frame - 1) == 0) + { + deltaStates[frame - 1].load(); + DEBUG_LOG(NETWORK, "Saved frame %d pages: %d ram, %d vram, %d aica ram", frame - 1, (u32)deltaStates[frame - 1].ram.size(), + (u32)deltaStates[frame - 1].vram.size(), (u32)deltaStates[frame - 1].aram.size()); + } +#ifdef SYNC_TEST + else + { + MemPages memPages; + memPages.load(); + const MemPages& savedPages = deltaStates[frame - 1]; + verify(memPages.ram.size() == savedPages.ram.size()); + for (const auto& pair : memPages.ram) + { + verify(savedPages.ram.count(pair.first) == 1); + verify(memcmp(&pair.second[0], &savedPages.ram.find(pair.first)->second[0], PAGE_SIZE) == 0); + } + verify(memPages.vram.size() == savedPages.vram.size()); + for (const auto& pair : memPages.vram) + { + verify(savedPages.vram.count(pair.first) == 1); + verify(memcmp(&pair.second[0], &savedPages.vram.find(pair.first)->second[0], PAGE_SIZE) == 0); + } + verify(memPages.aram.size() == savedPages.aram.size()); + for (const auto& pair : memPages.aram) + { + verify(savedPages.aram.count(pair.first) == 1); + verify(memcmp(&pair.second[0], &savedPages.aram.find(pair.first)->second[0], PAGE_SIZE) == 0); + } + } +#endif + } + memwatch::protect(); + + return true; +} + +/* + * log_game_state - Used in diagnostic testing. The client should use + * the ggpo_log function to write the contents of the specified save + * state in a human readible form. + */ +static bool log_game_state(char *filename, unsigned char *buffer, int len) +{ +#ifdef SYNC_TEST + static int lastLoggedFrame = -1; + static u8 *lastState; + int frame = *(u32 *)buffer; + DEBUG_LOG(NETWORK, "log_game_state frame %d len %d", frame, len); + if (lastLoggedFrame == frame) { + for (int i = 0; i < len; i++) + if (buffer[i] != lastState[i]) + { + WARN_LOG(NETWORK, "States for frame %d differ at offset %d: now %x prev %x", frame, i, *(u32 *)&buffer[i & ~3], *(u32 *)&lastState[i & ~3]); + break; + } + } + lastState = buffer; + lastLoggedFrame = frame; +#endif + + return true; +} + +/* + * free_buffer - Frees a game state allocated in save_game_state. You + * should deallocate the memory contained in the buffer. + */ +static void free_buffer(void *buffer) +{ + if (buffer != nullptr) + { + int frame = *(u32 *)buffer; + deltaStates.erase(frame); + free(buffer); + } +} + +void startSession(int localPort, int localPlayerNum) +{ + GGPOSessionCallbacks cb{}; + cb.begin_game = begin_game; + cb.advance_frame = advance_frame; + cb.load_game_state = load_game_state; + cb.save_game_state = save_game_state; + cb.free_buffer = free_buffer; + cb.on_event = on_event; + cb.log_game_state = log_game_state; + +#ifdef SYNC_TEST + GGPOErrorCode result = ggpo_start_synctest(&ggpoSession, &cb, config::Settings::instance().getGameId().c_str(), 2, sizeof(kcode[0]), 1); + if (result != GGPO_OK) + { + WARN_LOG(NETWORK, "GGPO start sync session failed: %d", result); + ggpoSession = nullptr; + return; + } + ggpo_idle(ggpoSession, 0); + ggpo::localPlayerNum = localPlayerNum; + GGPOPlayer player{ sizeof(GGPOPlayer), GGPO_PLAYERTYPE_LOCAL, localPlayerNum + 1 }; + result = ggpo_add_player(ggpoSession, &player, &localPlayer); + player.player_num = (1 - localPlayerNum) + 1; + result = ggpo_add_player(ggpoSession, &player, &remotePlayer); + synchronized = true; + NOTICE_LOG(NETWORK, "GGPO synctest session started"); +#else + GGPOErrorCode result = ggpo_start_session(&ggpoSession, &cb, config::Settings::instance().getGameId().c_str(), 2, sizeof(kcode[0]), localPort); + if (result != GGPO_OK) + { + WARN_LOG(NETWORK, "GGPO start session failed: %d", result); + ggpoSession = nullptr; + return; + } + + // automatically disconnect clients after 3000 ms and start our count-down timer + // for disconnects after 1000 ms. To completely disable disconnects, simply use + // a value of 0 for ggpo_set_disconnect_timeout. + ggpo_set_disconnect_timeout(ggpoSession, 3000); + ggpo_set_disconnect_notify_start(ggpoSession, 1000); + + ggpo::localPlayerNum = localPlayerNum; + GGPOPlayer player{ sizeof(GGPOPlayer), GGPO_PLAYERTYPE_LOCAL, localPlayerNum + 1 }; + result = ggpo_add_player(ggpoSession, &player, &localPlayer); + if (result != GGPO_OK) + { + WARN_LOG(NETWORK, "GGPO cannot add local player: %d", result); + ggpo_close_session(ggpoSession); + ggpoSession = nullptr; + return; + } +// ggpo_set_frame_delay(ggpoSession, localPlayer, FRAME_DELAY); + + size_t colon = config::NetworkServer.get().find(':'); + std::string peerIp = config::NetworkServer.get().substr(0, colon); + if (peerIp.empty()) + peerIp = "127.0.0.1"; + u32 peerPort; + if (colon == std::string::npos) + { + if (peerIp == "127.0.0.1") + peerPort = localPort ^ 1; + else + peerPort = 19713; + } + else + { + peerPort = atoi(config::NetworkServer.get().substr(colon + 1).c_str()); + } + player.type = GGPO_PLAYERTYPE_REMOTE; + strcpy(player.u.remote.ip_address, peerIp.c_str()); + player.u.remote.port = peerPort; + player.player_num = (1 - localPlayerNum) + 1; + result = ggpo_add_player(ggpoSession, &player, &remotePlayer); + if (result != GGPO_OK) + { + WARN_LOG(NETWORK, "GGPO cannot add remote player: %d", result); + ggpo_close_session(ggpoSession); + ggpoSession = nullptr; + } + DEBUG_LOG(NETWORK, "GGPO session started"); +#endif +} + +void stopSession() +{ + std::lock_guard lock(ggpoMutex); + if (ggpoSession == nullptr) + return; + ggpo_close_session(ggpoSession); + ggpoSession = nullptr; +} + +void getInput(u32 out_kcode[4]) +{ + // TODO need a std::recursive_mutex to use a lock here + memcpy(out_kcode, kcode, sizeof(kcode)); + if (ggpoSession == nullptr) + return; + // should not call any callback + u32 inputs[4]; + ggpo_synchronize_input(ggpoSession, (void *)&inputs[0], sizeof(inputs[0]) * 2, nullptr); // FIXME numPlayers + out_kcode[0] = ~inputs[0]; + out_kcode[1] = ~inputs[1]; + //out_kcode[2] = ~inputs[2]; + //out_kcode[3] = ~inputs[3]; +} + +void nextFrame() +{ + std::lock_guard lock(ggpoMutex); + if (ggpoSession == nullptr) + return; + // will call save_game_state + ggpo_advance_frame(ggpoSession); + + // may rollback + ggpo_idle(ggpoSession, 0); + // may call save_game_state + do { + u32 input = ~kcode[localPlayerNum]; + GGPOErrorCode result = ggpo_add_local_input(ggpoSession, localPlayer, &input, sizeof(input)); + if (result == GGPO_OK) + break; + WARN_LOG(NETWORK, "ggpo_add_local_input failed %d", result); + if (result != GGPO_ERRORCODE_PREDICTION_THRESHOLD) + { + ggpo_close_session(ggpoSession); + ggpoSession = nullptr; + throw FlycastException("GGPO error"); + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ggpo_idle(ggpoSession, 0); + } while (active()); +#ifdef SYNC_TEST + u32 input = ~kcode[1 - localPlayerNum]; + result = ggpo_add_local_input(ggpoSession, remotePlayer, &input, sizeof(input)); + if (result != GGPO_OK) + WARN_LOG(NETWORK, "ggpo_add_local_input(2) failed %d", result); +#endif +} + +bool active() +{ + return ggpoSession != nullptr; +} + +std::future startNetwork() +{ + synchronized = false; + return std::async(std::launch::async, []{ + { + std::lock_guard lock(ggpoMutex); +#ifdef SYNC_TEST + startSession(0, 0); +#else + if (config::ActAsServer) + startSession(19713, 0); + else + startSession(config::NetworkServer.get().empty() || config::NetworkServer.get() == "127.0.0.1" ? 19712 : 19713, 1); +#endif + } + while (!synchronized && active()) { + { + std::lock_guard lock(ggpoMutex); + if (ggpoSession == nullptr) + break; + ggpo_idle(ggpoSession, 0); + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } +#ifdef SYNC_TEST + // save initial state (frame 0) + if (active()) + getInput(); +#endif + return active(); + }); +} + +} + +#else // LIBRETRO +#include "types.h" +#include "ggpo.h" +#include "input/gamepad_device.h" + +namespace ggpo +{ + +void stopSession() { +} + +void getInput(u32 out_kcode[4]) { + memcpy(out_kcode, kcode, sizeof(kcode)); +} + +void nextFrame() { +} + +bool active() { + return false; +} + +std::future startNetwork() { + return std::async(std::launch::deferred, []{ return false; });; +} + +} +#endif diff --git a/core/network/ggpo.h b/core/network/ggpo.h new file mode 100644 index 000000000..c0f30f1da --- /dev/null +++ b/core/network/ggpo.h @@ -0,0 +1,32 @@ +/* + Copyright 2021 flyinghead + + This file is part of Flycast. + + Flycast 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 Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#include "types.h" +#include + +namespace ggpo +{ + +std::future startNetwork(); +void startSession(int localPort, int localPlayerNum); +void stopSession(); +void getInput(u32 out_kcode[4]); +void nextFrame(); +bool active(); + +} diff --git a/core/nullDC.cpp b/core/nullDC.cpp index d81e8eb05..4728fda2e 100644 --- a/core/nullDC.cpp +++ b/core/nullDC.cpp @@ -106,7 +106,7 @@ void dc_savestate(int index) } void *data_ptr = data; - + total_size = 0; if (!dc_serialize(&data_ptr, &total_size)) { WARN_LOG(SAVESTATE, "Failed to save state - could not serialize data") ; diff --git a/core/rend/TexCache.cpp b/core/rend/TexCache.cpp index ebfd87b4a..fcdf3196c 100644 --- a/core/rend/TexCache.cpp +++ b/core/rend/TexCache.cpp @@ -209,27 +209,22 @@ std::mutex vramlist_lock; void libCore_vramlock_Lock(u32 start_offset64, u32 end_offset64, BaseTextureCacheData *texture) { - vram_block* block=(vram_block* )malloc(sizeof(vram_block)); - - if (end_offset64>(VRAM_SIZE-1)) + if (end_offset64 > VRAM_SIZE - 1) { WARN_LOG(PVR, "vramlock_Lock_64: end_offset64>(VRAM_SIZE-1) \n Tried to lock area out of vram , possibly bug on the pvr plugin"); - end_offset64=(VRAM_SIZE-1); + end_offset64 = VRAM_SIZE - 1; } - if (start_offset64>end_offset64) + if (start_offset64 > end_offset64) { WARN_LOG(PVR, "vramlock_Lock_64: start_offset64>end_offset64 \n Tried to lock negative block , possibly bug on the pvr plugin"); - start_offset64=0; + return; } - - - block->end=end_offset64; - block->start=start_offset64; - block->len=end_offset64-start_offset64+1; - block->userdata = texture; - block->type=64; + vram_block *block = new vram_block(); + block->end = end_offset64; + block->start = start_offset64; + block->texture = texture; { std::lock_guard lock(vramlist_lock); @@ -979,9 +974,9 @@ template void WriteTextureToVRam<2, 1, 0, 3>(u32 width, u32 height, u8 *data, u1 static void rend_text_invl(vram_block* bl) { - BaseTextureCacheData* tcd = (BaseTextureCacheData*)bl->userdata; - tcd->dirty = FrameCount; - tcd->lock_block = nullptr; + BaseTextureCacheData* texture = bl->texture; + texture->dirty = FrameCount; + texture->lock_block = nullptr; libCore_vramlock_Unlock_block_wb(bl); } diff --git a/core/rend/TexCache.h b/core/rend/TexCache.h index b2e98bb3b..85898fed7 100644 --- a/core/rend/TexCache.h +++ b/core/rend/TexCache.h @@ -551,19 +551,18 @@ constexpr TexConvFP32 tex1555_VQ32 = texture_VQ>>; } +class BaseTextureCacheData; + struct vram_block { u32 start; u32 end; - u32 len; - u32 type; - void* userdata; + BaseTextureCacheData *texture; }; -class BaseTextureCacheData; - bool VramLockedWriteOffset(size_t offset); +bool VramLockedWrite(u8* address); void libCore_vramlock_Lock(u32 start_offset, u32 end_offset, BaseTextureCacheData *texture); void UpscalexBRZ(int factor, u32* source, u32* dest, int width, int height, bool has_alpha); diff --git a/core/rend/gui.cpp b/core/rend/gui.cpp index e59956992..711d56567 100644 --- a/core/rend/gui.cpp +++ b/core/rend/gui.cpp @@ -28,6 +28,7 @@ #include "gles/imgui_impl_opengl3.h" #include "imgui/roboto_medium.h" #include "network/naomi_network.h" +#include "network/ggpo.h" #include "wsi/context.h" #include "input/gamepad_device.h" #include "gui_util.h" @@ -41,7 +42,6 @@ #include "emulator.h" #include "rend/mainui.h" -extern void UpdateInputState(); static bool game_started; extern u8 kb_shift[MAPLE_PORTS]; // shift keys pressed (bitmask) @@ -310,8 +310,6 @@ static void ImGui_Impl_NewFrame() ImGuiIO& io = ImGui::GetIO(); - UpdateInputState(); - // Read keyboard modifiers inputs io.KeyCtrl = (kb_shift[0] & (0x01 | 0x10)) != 0; io.KeyShift = (kb_shift[0] & (0x02 | 0x20)) != 0; @@ -1576,7 +1574,6 @@ static void gui_display_settings() if (ImGui::BeginTabItem("Audio")) { ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, normal_padding); - OptionCheckbox("Disable Sound", config::DisableSound, "Disable the emulator sound output"); OptionCheckbox("Enable DSP", config::DSPEnabled, "Enable the Dreamcast Digital Sound Processor. Only recommended on fast platforms"); #ifdef __ANDROID__ @@ -1708,9 +1705,22 @@ static void gui_display_settings() { OptionCheckbox("Broadband Adapter Emulation", config::EmulateBBA, "Emulate the Ethernet Broadband Adapter (BBA) instead of the Modem"); + OptionCheckbox("Enable GGPO Networking", config::GGPOEnable, + "Enable networking using GGPO"); OptionCheckbox("Enable Naomi Networking", config::NetworkEnable, "Enable networking for supported Naomi games"); - if (config::NetworkEnable) + if (config::GGPOEnable) + { + OptionCheckbox("Play as player 1", config::ActAsServer, + "Deselect to play as player 2"); + char server_name[256]; + strcpy(server_name, config::NetworkServer.get().c_str()); + ImGui::InputText("Peer", server_name, sizeof(server_name), ImGuiInputTextFlags_CharsNoBlank, nullptr, nullptr); + ImGui::SameLine(); + ShowHelpMarker("Your peer IP address and optional port"); + config::NetworkServer.set(server_name); + } + else if (config::NetworkEnable) { OptionCheckbox("Act as Server", config::ActAsServer, "Create a local server for Naomi network games"); @@ -2059,7 +2069,10 @@ static void gui_network_start() ImGui::SetCursorPosY(126.f * scaling); if (ImGui::Button("Cancel", ImVec2(100.f * scaling, 0.f))) { - naomiNetwork.terminate(); + if (config::GGPOEnable) + ggpo::stopSession(); + else + naomiNetwork.terminate(); networkStatus.get(); gui_state = GuiState::Main; settings.imgread.ImagePath[0] = '\0'; @@ -2086,7 +2099,12 @@ static void gui_display_loadscreen() { try { dc_get_load_status(); - if (NaomiNetworkSupported()) + if (config::GGPOEnable) + { + networkStatus = ggpo::startNetwork(); + gui_state = GuiState::NetworkStart; + } + else if (NaomiNetworkSupported()) { start_network(); } diff --git a/core/rend/mainui.cpp b/core/rend/mainui.cpp index 4447bcfb6..e5afd974e 100644 --- a/core/rend/mainui.cpp +++ b/core/rend/mainui.cpp @@ -35,6 +35,7 @@ void UpdateInputState(); bool mainui_rend_frame() { os_DoEvents(); + UpdateInputState(); if (gui_is_open() || gui_state == GuiState::VJoyEdit) { @@ -48,7 +49,6 @@ bool mainui_rend_frame() { if (!rend_single_frame(mainui_enabled)) { - UpdateInputState(); if (!dc_is_running()) { std::string error = dc_get_last_error(); diff --git a/core/serialize.cpp b/core/serialize.cpp index 76cb728b5..a0e2c5d4a 100644 --- a/core/serialize.cpp +++ b/core/serialize.cpp @@ -20,6 +20,7 @@ #include "hw/naomi/naomi.h" #include "hw/naomi/naomi_cart.h" #include "hw/sh4/sh4_cache.h" +#include "hw/sh4/sh4_interpreter.h" #include "hw/bba/bba.h" #include "cfg/option.h" @@ -152,6 +153,7 @@ extern Sh4RCB* p_sh4rcb; //./core/hw/sh4/sh4_sched.o extern u64 sh4_sched_ffb; extern std::vector sch_list; +extern int sh4_sched_next_id; //./core/hw/sh4/interpr/sh4_interpreter.o extern int aica_schid; @@ -236,14 +238,18 @@ bool register_unserialize(T& regs,void **data, unsigned int *total_size, seriali return true; } -bool dc_serialize(void **data, unsigned int *total_size) +static const std::array getSchedulerIds() { + return { aica_schid, rtc_schid, gdrom_schid, maple_schid, dma_sched_id, + tmu_sched[0], tmu_sched[1], tmu_sched[2], render_end_schid, vblank_schid, + modem_sched }; +} + +bool dc_serialize(void **data, unsigned int *total_size, bool rollback) { int i = 0; serialize_version_enum version = VCUR_FLYCAST; - *total_size = 0 ; - //dc not initialized yet if ( p_sh4rcb == NULL ) return false ; @@ -260,6 +266,7 @@ bool dc_serialize(void **data, unsigned int *total_size) REICAST_S(armFiqEnable); REICAST_S(armMode); REICAST_S(Arm7Enabled); + REICAST_S(arm7ClockTicks); dsp::state.serialize(data, total_size); @@ -269,7 +276,8 @@ bool dc_serialize(void **data, unsigned int *total_size) REICAST_S(timers[i].m_step); } - REICAST_SA(aica_ram.data,aica_ram.size) ; + if (!rollback) + REICAST_SA(aica_ram.data,aica_ram.size) ; REICAST_S(VREG); REICAST_S(ARMRST); REICAST_S(rtc_EN); @@ -341,7 +349,8 @@ bool dc_serialize(void **data, unsigned int *total_size) SerializeTAContext(data, total_size); - REICAST_SA(vram.data, vram.size); + if (!rollback) + REICAST_SA(vram.data, vram.size); REICAST_SA(OnChipRAM.data(), OnChipRAM_SIZE); @@ -358,7 +367,8 @@ bool dc_serialize(void **data, unsigned int *total_size) icache.Serialize(data, total_size); ocache.Serialize(data, total_size); - REICAST_SA(mem_b.data, mem_b.size); + if (!rollback) + REICAST_SA(mem_b.data, mem_b.size); REICAST_SA(InterruptEnvId,32); REICAST_SA(InterruptBit,32); @@ -385,43 +395,23 @@ bool dc_serialize(void **data, unsigned int *total_size) REICAST_S((*p_sh4rcb).cntx); + REICAST_S(sh4InterpCycles); REICAST_S(sh4_sched_ffb); + std::array schedIds = getSchedulerIds(); + if (sh4_sched_next_id == -1) + REICAST_S(sh4_sched_next_id); + else + for (u32 i = 0; i < schedIds.size(); i++) + if (schedIds[i] == sh4_sched_next_id) + REICAST_S(i); - REICAST_S(sch_list[aica_schid].tag) ; - REICAST_S(sch_list[aica_schid].start) ; - REICAST_S(sch_list[aica_schid].end) ; - - REICAST_S(sch_list[rtc_schid].tag) ; - REICAST_S(sch_list[rtc_schid].start) ; - REICAST_S(sch_list[rtc_schid].end) ; - - REICAST_S(sch_list[gdrom_schid].tag) ; - REICAST_S(sch_list[gdrom_schid].start) ; - REICAST_S(sch_list[gdrom_schid].end) ; - - REICAST_S(sch_list[maple_schid].tag) ; - REICAST_S(sch_list[maple_schid].start) ; - REICAST_S(sch_list[maple_schid].end) ; - - REICAST_S(sch_list[dma_sched_id].tag) ; - REICAST_S(sch_list[dma_sched_id].start) ; - REICAST_S(sch_list[dma_sched_id].end) ; - - for (int i = 0; i < 3; i++) + for (u32 i = 0; i < schedIds.size() - 1; i++) { - REICAST_S(sch_list[tmu_sched[i]].tag) ; - REICAST_S(sch_list[tmu_sched[i]].start) ; - REICAST_S(sch_list[tmu_sched[i]].end) ; + REICAST_S(sch_list[schedIds[i]].tag); + REICAST_S(sch_list[schedIds[i]].start); + REICAST_S(sch_list[schedIds[i]].end); } - REICAST_S(sch_list[render_end_schid].tag) ; - REICAST_S(sch_list[render_end_schid].start) ; - REICAST_S(sch_list[render_end_schid].end) ; - - REICAST_S(sch_list[vblank_schid].tag) ; - REICAST_S(sch_list[vblank_schid].start) ; - REICAST_S(sch_list[vblank_schid].end) ; - REICAST_S(config::EmulateBBA.get()); if (config::EmulateBBA) { @@ -493,6 +483,7 @@ static bool dc_unserialize_libretro(void **data, unsigned int *total_size, seria REICAST_SKIP(1); // stopState REICAST_SKIP(1); // holdState } + arm7ClockTicks = 0; dsp::state.deserialize(data, total_size, version); @@ -713,6 +704,7 @@ static bool dc_unserialize_libretro(void **data, unsigned int *total_size, seria REICAST_SKIP(4); // old_dn } + sh4InterpCycles = 0; REICAST_US(sh4_sched_ffb); if (version < V9_LIBRETRO) REICAST_SKIP(4); // sh4_sched_intr @@ -865,14 +857,12 @@ static bool dc_unserialize_libretro(void **data, unsigned int *total_size, seria return true; } -bool dc_unserialize(void **data, unsigned int *total_size) +bool dc_unserialize(void **data, unsigned int *total_size, bool rollback) { int i = 0; serialize_version_enum version = V1 ; - *total_size = 0 ; - REICAST_US(version) ; if (version >= V5_LIBRETRO && version <= V13_LIBRETRO) return dc_unserialize_libretro(data, total_size, version); @@ -901,6 +891,10 @@ bool dc_unserialize(void **data, unsigned int *total_size) REICAST_US(Arm7Enabled); if (version < V5) REICAST_SKIP(256 + 3); + if (version >= V19) + REICAST_US(arm7ClockTicks); + else + arm7ClockTicks = 0; dsp::state.deserialize(data, total_size, version); @@ -910,7 +904,8 @@ bool dc_unserialize(void **data, unsigned int *total_size) REICAST_US(timers[i].m_step); } - REICAST_USA(aica_ram.data,aica_ram.size) ; + if (!rollback) + REICAST_USA(aica_ram.data,aica_ram.size) ; REICAST_US(VREG); REICAST_US(ARMRST); REICAST_US(rtc_EN); @@ -1036,7 +1031,8 @@ bool dc_unserialize(void **data, unsigned int *total_size) if (version >= V11) UnserializeTAContext(data, total_size, version); - REICAST_USA(vram.data, vram.size); + if (!rollback) + REICAST_USA(vram.data, vram.size); pal_needs_update = true; REICAST_USA(OnChipRAM.data(), OnChipRAM_SIZE); @@ -1060,7 +1056,8 @@ bool dc_unserialize(void **data, unsigned int *total_size) else ocache.Reset(true); - REICAST_USA(mem_b.data, mem_b.size); + if (!rollback) + REICAST_USA(mem_b.data, mem_b.size); if (version < V5) REICAST_SKIP(2); @@ -1089,46 +1086,30 @@ bool dc_unserialize(void **data, unsigned int *total_size) REICAST_SKIP(4); REICAST_SKIP(4); } + if (version >= V19) + REICAST_US(sh4InterpCycles); + else + sh4InterpCycles = 0; REICAST_US(sh4_sched_ffb); + std::array schedIds = getSchedulerIds(); + + if (version >= V19) + { + REICAST_US(sh4_sched_next_id); + if (sh4_sched_next_id != -1) + sh4_sched_next_id = schedIds[sh4_sched_next_id]; + } if (version < V8) REICAST_US(i); // sh4_sched_intr - REICAST_US(sch_list[aica_schid].tag) ; - REICAST_US(sch_list[aica_schid].start) ; - REICAST_US(sch_list[aica_schid].end) ; - - REICAST_US(sch_list[rtc_schid].tag) ; - REICAST_US(sch_list[rtc_schid].start) ; - REICAST_US(sch_list[rtc_schid].end) ; - - REICAST_US(sch_list[gdrom_schid].tag) ; - REICAST_US(sch_list[gdrom_schid].start) ; - REICAST_US(sch_list[gdrom_schid].end) ; - - REICAST_US(sch_list[maple_schid].tag) ; - REICAST_US(sch_list[maple_schid].start) ; - REICAST_US(sch_list[maple_schid].end) ; - - REICAST_US(sch_list[dma_sched_id].tag) ; - REICAST_US(sch_list[dma_sched_id].start) ; - REICAST_US(sch_list[dma_sched_id].end) ; - - for (int i = 0; i < 3; i++) + for (u32 i = 0; i < schedIds.size() - 1; i++) { - REICAST_US(sch_list[tmu_sched[i]].tag) ; - REICAST_US(sch_list[tmu_sched[i]].start) ; - REICAST_US(sch_list[tmu_sched[i]].end) ; + REICAST_US(sch_list[schedIds[i]].tag); + REICAST_US(sch_list[schedIds[i]].start); + REICAST_US(sch_list[schedIds[i]].end); } - REICAST_US(sch_list[render_end_schid].tag) ; - REICAST_US(sch_list[render_end_schid].start) ; - REICAST_US(sch_list[render_end_schid].end) ; - - REICAST_US(sch_list[vblank_schid].tag) ; - REICAST_US(sch_list[vblank_schid].start) ; - REICAST_US(sch_list[vblank_schid].end) ; - if (version < V8) { REICAST_US(i); // sch_list[time_sync].tag @@ -1174,7 +1155,7 @@ bool dc_unserialize(void **data, unsigned int *total_size) REICAST_USA(UTLB,64); REICAST_USA(ITLB,4); if (version >= V11) - REICAST_USA(sq_remap,64); + REICAST_US(sq_remap); REICAST_USA(ITLB_LRU_USE,64); REICAST_US(NullDriveDiscType); diff --git a/core/types.h b/core/types.h index 63cf58752..a1f6c818a 100644 --- a/core/types.h +++ b/core/types.h @@ -188,8 +188,8 @@ void os_DebugBreak(); bool rc_serialize(const void *src, unsigned int src_size, void **dest, unsigned int *total_size) ; bool rc_unserialize(void *src, unsigned int src_size, void **dest, unsigned int *total_size); -bool dc_serialize(void **data, unsigned int *total_size); -bool dc_unserialize(void **data, unsigned int *total_size); +bool dc_serialize(void **data, unsigned int *total_size, bool rollback = false); +bool dc_unserialize(void **data, unsigned int *total_size, bool rollback = false); #define REICAST_S(v) rc_serialize(&(v), sizeof(v), data, total_size) #define REICAST_US(v) rc_unserialize(&(v), sizeof(v), data, total_size) @@ -339,6 +339,7 @@ struct settings_t struct { bool NoBatch; + bool muteAudio; } aica; struct @@ -353,6 +354,8 @@ struct settings_t } input; bool gameStarted; + bool endOfFrame; + bool disableRenderer; }; extern settings_t settings; @@ -474,5 +477,6 @@ enum serialize_version_enum { V16 = 811, V17 = 812, V18 = 813, - VCUR_FLYCAST = V18, + V19 = 814, + VCUR_FLYCAST = V19, }; diff --git a/core/windows/fault_handler.cpp b/core/windows/fault_handler.cpp index 52e048282..22bed318d 100644 --- a/core/windows/fault_handler.cpp +++ b/core/windows/fault_handler.cpp @@ -17,11 +17,11 @@ #include "oslib/oslib.h" #include "hw/sh4/dyna/blockmanager.h" #include "hw/sh4/dyna/ngen.h" +#include "rend/TexCache.h" +#include "hw/mem/_vmem.h" +#include "hw/mem/mem_watch.h" #include -bool VramLockedWrite(u8* address); -bool BM_LockedWrite(u8* address); - static PVOID vectoredHandler; static LONG (WINAPI *prevExceptionHandler)(EXCEPTION_POINTERS *ep); @@ -79,6 +79,9 @@ static LONG WINAPI exceptionHandler(EXCEPTION_POINTERS *ep) EXCEPTION_RECORD* pExceptionRecord = ep->ExceptionRecord; u8* address = (u8 *)pExceptionRecord->ExceptionInformation[1]; + // Ram watcher for net rollback + if (memwatch::writeAccess(address)) + return EXCEPTION_CONTINUE_EXECUTION; // code protection in RAM if (bm_RamWriteAccess(address)) return EXCEPTION_CONTINUE_EXECUTION; diff --git a/shell/libretro/option.cpp b/shell/libretro/option.cpp index 906343b73..e7d4ab618 100644 --- a/shell/libretro/option.cpp +++ b/shell/libretro/option.cpp @@ -42,7 +42,6 @@ Option SavestateSlot(""); // Sound Option DSPEnabled(CORE_OPTION_NAME "_enable_dsp", false); -Option DisableSound(""); #if HOST_CPU == CPU_ARM Option AudioBufferSize("", 5644); // 128 ms #else @@ -109,6 +108,7 @@ Option ActAsServer("", false); OptionString DNS("", "46.101.91.123"); OptionString NetworkServer("", ""); Option EmulateBBA("", false); // TODO +Option GGPOEnable("", false); // Maple diff --git a/tests/src/serialize_test.cpp b/tests/src/serialize_test.cpp index 4247a3eb1..fa4e2dbd7 100644 --- a/tests/src/serialize_test.cpp +++ b/tests/src/serialize_test.cpp @@ -31,7 +31,7 @@ TEST_F(SerializeTest, SizeTest) unsigned int total_size = 0; void *data = nullptr; ASSERT_TRUE(dc_serialize(&data, &total_size)); - ASSERT_EQ(28187879u, total_size); + ASSERT_EQ(28187891u, total_size); }