net rollback with ggpo

This commit is contained in:
Flyinghead 2021-09-02 17:51:23 +02:00
parent f85bca06d7
commit 69d0801121
68 changed files with 6131 additions and 186 deletions

View File

@ -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)

View File

@ -41,7 +41,6 @@ Option<int> SavestateSlot("Dreamcast.SavestateSlot");
// Sound
Option<bool> DSPEnabled("aica.DSPEnabled", false);
Option<bool> DisableSound("aica.NoSound");
#if HOST_CPU == CPU_ARM
Option<int> AudioBufferSize("aica.BufferSize", 5644); // 128 ms
#else
@ -117,6 +116,7 @@ Option<bool> ActAsServer("ActAsServer", false, "network");
OptionString DNS("DNS", "46.101.91.123", "network");
OptionString NetworkServer("server", "", "network");
Option<bool> EmulateBBA("EmulateBBA", false, "network");
Option<bool> GGPOEnable("GGPO", false, "network");
#ifdef SUPPORT_DISPMANX
Option<bool> DispmanxMaintainAspect("maintain_aspect", true, "dispmanx");

View File

@ -307,7 +307,6 @@ extern Option<int> SavestateSlot;
constexpr bool LimitFPS = true;
extern Option<bool> DSPEnabled;
extern Option<bool> DisableSound;
extern Option<int> AudioBufferSize; //In samples ,*4 for bytes
extern Option<bool> AutoLatency;
@ -412,6 +411,7 @@ extern Option<bool> ActAsServer;
extern OptionString DNS;
extern OptionString NetworkServer;
extern Option<bool> EmulateBBA;
extern Option<bool> GGPOEnable;
#ifdef SUPPORT_DISPMANX
extern Option<bool> DispmanxMaintainAspect;

View File

@ -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 <stdarg.h>
// 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

View File

@ -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

View File

@ -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 <chrono>
#include <thread>
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;
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}

View File

@ -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<SavedInfo, 32> _saved_frames;
};
#endif

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}

View File

@ -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 <stdio.h>
#include <memory.h>
// 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

View File

@ -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<PollSinkCb, 16> _msg_sinks;
StaticBuffer<PollSinkCb, 16> _loop_sinks;
StaticBuffer<PollPeriodicSinkCb, 16> _periodic_sinks;
};
#endif

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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 <string>
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);
}

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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 &current = _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();
}
}

View File

@ -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<QueueEntry, 64> _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<GameInput, 64> _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<UdpProtocol::Event, 64> _event_queue;
};
#endif

View File

@ -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 <time.h>
#include <strings.h>
#include <chrono>
uint32_t Platform::GetCurrentTimeMS()
{
using namespace std::chrono;
static steady_clock::time_point startTime = steady_clock::now();
return (uint32_t)duration_cast<milliseconds>(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

View File

@ -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 <limits>
#include <stdio.h>
#include <stdarg.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <cstdint>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
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<int>::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

View File

@ -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

View File

@ -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 <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#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

View File

@ -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 <chrono>
#include <thread>
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;
}

View File

@ -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 T, int N> class RingBuffer
{
public:
RingBuffer<T, N>() :
_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

View File

@ -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 T, int N> class StaticBuffer
{
public:
StaticBuffer<T, N>() :
_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

View File

@ -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;
}

View File

@ -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, 32> _event_queue;
UdpMsg::connect_status *_local_connect_status;
};
#endif

View File

@ -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);
}

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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();

View File

@ -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;

View File

@ -70,6 +70,8 @@ private:
maple_device* dev;
};
extern u32 maple_kcode[4];
void mcfg_CreateDevices();
void mcfg_CreateNAOMIJamma();
void mcfg_CreateAtomisWaveControllers();

View File

@ -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;

View File

@ -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

View File

@ -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);

111
core/hw/mem/mem_watch.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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;
}
}

184
core/hw/mem/mem_watch.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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 <array>
#include <unordered_map>
namespace memwatch
{
using PageMap = std::unordered_map<u32, std::array<u8, PAGE_SIZE>>;
template<typename T>
class Watcher
{
bool started;
PageMap pages;
public:
void protect()
{
if (!started)
{
static_cast<T&>(*this).protectMem(0, 0xffffffff);
started = true;
}
else
{
for (const auto& pair : pages)
static_cast<T&>(*this).protectMem(pair.first, PAGE_SIZE);
}
pages.clear();
}
void reset()
{
started = false;
pages.clear();
}
bool hit(void *addr)
{
u32 offset = static_cast<T&>(*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<T&>(*this).getMemPage(offset), PAGE_SIZE);
static_cast<T&>(*this).unprotectMem(offset, PAGE_SIZE);
return true;
}
const PageMap& getPages() {
return pages;
}
};
class VramWatcher : public Watcher<VramWatcher>
{
friend class Watcher<VramWatcher>;
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<RamWatcher>
{
friend class Watcher<RamWatcher>;
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<AicaRamWatcher>
{
friend class Watcher<AicaRamWatcher>;
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();
}
}

View File

@ -5,6 +5,7 @@
#include "hw/pvr/pvr_mem.h"
#include "rend/TexCache.h"
#include "cfg/option.h"
#include "network/ggpo.h"
#include <mutex>
#include <zlib.h>
@ -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();
}

View File

@ -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;
}

View File

@ -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<RuntimeBlockInfo*>& block_list = blocks_per_page[addr / PAGE_SIZE];
std::vector<RuntimeBlockInfo*> 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<RuntimeBlockInfo*> 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;
}

View File

@ -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);

View File

@ -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");
}

View File

@ -45,3 +45,5 @@ void ExecuteDelayslot_RTE();
int UpdateSystem();
int UpdateSystem_INTC();
extern s32 sh4InterpCycles;

View File

@ -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 <ucontext.h>
@ -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;

500
core/network/ggpo.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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 <string.h>
#include <chrono>
#include <thread>
#include <mutex>
#include <unordered_map>
#include <xxhash.h>
//#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<int, MemPages> 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<std::mutex> 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<std::mutex> 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<bool> startNetwork()
{
synchronized = false;
return std::async(std::launch::async, []{
{
std::lock_guard<std::mutex> 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<std::mutex> 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<bool> startNetwork() {
return std::async(std::launch::deferred, []{ return false; });;
}
}
#endif

32
core/network/ggpo.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "types.h"
#include <future>
namespace ggpo
{
std::future<bool> startNetwork();
void startSession(int localPort, int localPlayerNum);
void stopSession();
void getInput(u32 out_kcode[4]);
void nextFrame();
bool active();
}

View File

@ -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") ;

View File

@ -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<std::mutex> 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);
}

View File

@ -551,19 +551,18 @@ constexpr TexConvFP32 tex1555_VQ32 = texture_VQ<ConvertTwiddle<Unpacker1555_32<B
constexpr TexConvFP32 tex4444_VQ32 = texture_VQ<ConvertTwiddle<Unpacker4444_32<BGRAPacker>>>;
}
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);

View File

@ -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();
}

View File

@ -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();

View File

@ -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<sched_list> 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<int, 11> 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<int, 11> 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<int, 11> 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);

View File

@ -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,
};

View File

@ -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 <windows.h>
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;

View File

@ -42,7 +42,6 @@ Option<int> SavestateSlot("");
// Sound
Option<bool> DSPEnabled(CORE_OPTION_NAME "_enable_dsp", false);
Option<bool> DisableSound("");
#if HOST_CPU == CPU_ARM
Option<int> AudioBufferSize("", 5644); // 128 ms
#else
@ -109,6 +108,7 @@ Option<bool> ActAsServer("", false);
OptionString DNS("", "46.101.91.123");
OptionString NetworkServer("", "");
Option<bool> EmulateBBA("", false); // TODO
Option<bool> GGPOEnable("", false);
// Maple

View File

@ -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);
}