IPC: implement batch command processing

This commit is contained in:
Gauvain 'GovanifY' Roussel-Tarbouriech 2020-08-17 16:29:16 +02:00 committed by tellowkrinkle
parent 86757fd36f
commit 89ce774d7e
2 changed files with 178 additions and 113 deletions

View File

@ -105,13 +105,11 @@ SocketIPC::SocketIPC(SysCoreThread* vm)
void SocketIPC::ExecuteTaskInThread() void SocketIPC::ExecuteTaskInThread()
{ {
int msgsock = 0; int msgsock = 0;
// for the sake of speed we malloc once a return buffer and reuse it by just
// cropping its size when needed, it is 450k long which is the size of 50k // we allocate once buffers to not have to do mallocs for each IPC
// MsgWrite64 replies, should be good enough even if we implement batch IPC // request, as malloc is expansive when we optimize for µs.
// processing. Coincidentally 650k is the size of 50k MsgWrite64 REQUESTS so m_ret_buffer = new char[MAX_IPC_RETURN_SIZE];
// we just allocate a 1mb buffer in the end, lul m_ipc_buffer = new char[MAX_IPC_SIZE];
ret_buffer = (char*)malloc(450000 * sizeof(char));
ipc_buffer = (char*)malloc(650000 * sizeof(char));
while (true) while (true)
{ {
msgsock = accept(m_sock, 0, 0); msgsock = accept(m_sock, 0, 0);
@ -121,14 +119,14 @@ void SocketIPC::ExecuteTaskInThread()
} }
else else
{ {
if (read_portable(msgsock, ipc_buffer, 650000) < 0) if (read_portable(msgsock, m_ipc_buffer, 650000) < 0)
{ {
return; return;
} }
else else
{ {
auto res = ParseCommand(ipc_buffer, ret_buffer); auto res = ParseCommand(m_ipc_buffer, m_ret_buffer);
if (write_portable(msgsock, res.second, res.first) < 0) if (write_portable(msgsock, res.buffer, res.size) < 0)
{ {
return; return;
} }
@ -146,8 +144,8 @@ SocketIPC::~SocketIPC()
close(m_sock); close(m_sock);
unlink(SOCKET_NAME); unlink(SOCKET_NAME);
#endif #endif
free(ret_buffer); delete[] m_ret_buffer;
free(ipc_buffer); delete[] m_ipc_buffer;
// destroy the thread // destroy the thread
try try
{ {
@ -158,99 +156,126 @@ SocketIPC::~SocketIPC()
char* SocketIPC::MakeOkIPC(char* ret_buffer) char* SocketIPC::MakeOkIPC(char* ret_buffer)
{ {
ret_buffer[0] = (unsigned char)IPC_OK; ret_buffer[0] = IPC_OK;
return ret_buffer; return ret_buffer;
} }
char* SocketIPC::MakeFailIPC(char* ret_buffer) char* SocketIPC::MakeFailIPC(char* ret_buffer)
{ {
ret_buffer[0] = (unsigned char)IPC_FAIL; ret_buffer[0] = IPC_FAIL;
return ret_buffer; return ret_buffer;
} }
std::pair<int, char*> SocketIPC::ParseCommand(char* buf, char* ret_buffer) SocketIPC::IPCBuffer SocketIPC::ParseCommand(char* buf, char* ret_buffer)
{ {
// currently all our instructions require a running VM so we check once // currently all our instructions require a running VM so we check once
// here, will help perf when/if we implement multi-ipc processing in one // here, will help perf when/if we implement multi-ipc processing in one
// socket roundtrip. // socket roundtrip.
if (!m_vm->HasActiveMachine()) if (!m_vm->HasActiveMachine())
return std::make_pair(1, MakeFailIPC(ret_buffer)); return IPCBuffer{1, MakeFailIPC(ret_buffer)};
// IPC Message event (1 byte)
// | Memory address (4 byte)
// | | argument (VLE)
// | | |
// format: XX YY YY YY YY ZZ ZZ ZZ ZZ
// reply code: 00 = OK, FF = NOT OK
// | return value (VLE)
// | |
// reply: XX ZZ ZZ ZZ ZZ
IPCCommand opcode = (IPCCommand)buf[0];
// return value u16 batch = 1;
std::pair<int, char*> rval; u32 ret_cnt = 1;
if ((IPCCommand)buf[0] == MsgMultiCommand)
// YY YY YY YY from schema above
u32 a = FromArray<u32>(buf, 1);
switch (opcode)
{ {
case MsgRead8: batch = FromArray<u16>(buf, 1);
buf += 3;
}
for (u16 i = 0; i < batch; i++)
{
// YY YY YY YY from schema below
u32 a = FromArray<u32>(buf, 1);
// IPC Message event (1 byte)
// | Memory address (4 byte)
// | | argument (VLE)
// | | |
// format: XX YY YY YY YY ZZ ZZ ZZ ZZ
// reply code: 00 = OK, FF = NOT OK
// | return value (VLE)
// | |
// reply: XX ZZ ZZ ZZ ZZ
//
// NB: memory safety checking would be very expansive in our case,
// implemented per command and simply a mess. As our threat model is
// nonexistant, knowing we can disable the IPC at any time and having
// checks client-side it simply makes more sense to not do check
// server-side, as bad as this sounds.
// Re security threat model: we control the entire emulated memory of
// the emulated game, we can DoS our emulator easily by abusing the IPC
// features already, and regardless of where you read this there's
// probably no sandbox implemented, so simply being able to write to the
// game memory code region would make us control the JIT and have probably
// full access over the host machine.
switch ((IPCCommand)buf[0])
{ {
u8 res; case MsgRead8:
res = memRead8(a); {
rval = std::make_pair(2, ToArray(MakeOkIPC(ret_buffer), res, 1)); u8 res;
break; res = memRead8(a);
} ToArray(ret_buffer, res, ret_cnt);
case MsgRead16: ret_cnt += 1;
{ buf += 5;
u16 res; break;
res = memRead16(a); }
rval = std::make_pair(3, ToArray(MakeOkIPC(ret_buffer), res, 1)); case MsgRead16:
break; {
} u16 res;
case MsgRead32: res = memRead16(a);
{ ToArray(ret_buffer, res, ret_cnt);
u32 res; ret_cnt += 2;
res = memRead32(a); buf += 5;
rval = std::make_pair(5, ToArray(MakeOkIPC(ret_buffer), res, 1)); break;
break; }
} case MsgRead32:
case MsgRead64: {
{ u32 res;
u64 res; res = memRead32(a);
memRead64(a, &res); ToArray(ret_buffer, res, ret_cnt);
rval = std::make_pair(9, ToArray(MakeOkIPC(ret_buffer), res, 1)); ret_cnt += 4;
break; buf += 5;
} break;
case MsgWrite8: }
{ case MsgRead64:
memWrite8(a, FromArray<u8>(buf, 5)); {
rval = std::make_pair(1, MakeOkIPC(ret_buffer)); u64 res;
break; memRead64(a, &res);
} ToArray(ret_buffer, res, ret_cnt);
case MsgWrite16: ret_cnt += 8;
{ buf += 5;
memWrite16(a, FromArray<u16>(buf, 5)); break;
rval = std::make_pair(1, MakeOkIPC(ret_buffer)); }
break; case MsgWrite8:
} {
case MsgWrite32: memWrite8(a, FromArray<u8>(buf, 5));
{ buf += 6;
memWrite32(a, FromArray<u32>(buf, 5)); break;
rval = std::make_pair(1, MakeOkIPC(ret_buffer)); }
break; case MsgWrite16:
} {
case MsgWrite64: memWrite16(a, FromArray<u16>(buf, 5));
{ buf += 7;
memWrite64(a, FromArray<u64>(buf, 5)); break;
rval = std::make_pair(1, MakeOkIPC(ret_buffer)); }
break; case MsgWrite32:
} {
default: memWrite32(a, FromArray<u32>(buf, 5));
{ buf += 9;
rval = std::make_pair(1, MakeFailIPC(ret_buffer)); break;
break; }
case MsgWrite64:
{
memWrite64(a, FromArray<u64>(buf, 5));
buf += 13;
break;
}
default:
{
return IPCBuffer{1, MakeFailIPC(ret_buffer)};
}
} }
} }
return rval; return IPCBuffer{(int)ret_cnt, MakeOkIPC(ret_buffer)};
} }

View File

@ -26,6 +26,7 @@ using namespace Threading;
class SocketIPC : public pxThread class SocketIPC : public pxThread
{ {
// parent thread
typedef pxThread _parent; typedef pxThread _parent;
protected: protected:
@ -33,41 +34,80 @@ protected:
// windows claim to have support for AF_UNIX sockets but that is a blatant lie, // windows claim to have support for AF_UNIX sockets but that is a blatant lie,
// their SDK won't even run their own examples, so we go on TCP sockets. // their SDK won't even run their own examples, so we go on TCP sockets.
#define PORT 28011 #define PORT 28011
SOCKET m_sock = INVALID_SOCKET;
#else #else
// absolute path of the socket. Stored in the temporary directory in linux since // absolute path of the socket. Stored in the temporary directory in linux since
// /run requires superuser permission // /run requires superuser permission
const char* SOCKET_NAME = "/tmp/pcsx2.sock"; const char* SOCKET_NAME = "/tmp/pcsx2.sock";
#endif
// socket handlers
#ifdef _WIN32
SOCKET m_sock = INVALID_SOCKET;
#else
int m_sock = 0; int m_sock = 0;
#endif #endif
// buffers that store the ipc request and reply messages.
char* ret_buffer;
char* ipc_buffer;
// possible command messages /**
enum IPCCommand * Maximum memory used by an IPC message request.
* Equivalent to 50,000 Write64 requests.
*/
const unsigned int MAX_IPC_SIZE = 650000;
/**
* Maximum memory used by an IPC message reply.
* Equivalent to 50,000 Read64 replies.
*/
const unsigned int MAX_IPC_RETURN_SIZE = 450000;
/**
* IPC return buffer.
* A preallocated buffer used to store all IPC replies.
* to the size of 50.000 MsgWrite64 IPC calls.
*/
char* m_ret_buffer;
/**
* IPC messages buffer.
* A preallocated buffer used to store all IPC messages.
*/
char* m_ipc_buffer;
/**
* IPC Command messages opcodes.
* A list of possible operations possible by the IPC.
* Each one of them is what we call an "opcode" and is the first
* byte sent by the IPC to differentiate between commands.
*/
enum IPCCommand : unsigned char
{ {
MsgRead8 = 0, MsgRead8 = 0, /**< Read 8 bit value to memory. */
MsgRead16 = 1, MsgRead16 = 1, /**< Read 16 bit value to memory. */
MsgRead32 = 2, MsgRead32 = 2, /**< Read 32 bit value to memory. */
MsgRead64 = 3, MsgRead64 = 3, /**< Read 64 bit value to memory. */
MsgWrite8 = 4, MsgWrite8 = 4, /**< Write 8 bit value to memory. */
MsgWrite16 = 5, MsgWrite16 = 5, /**< Write 16 bit value to memory. */
MsgWrite32 = 6, MsgWrite32 = 6, /**< Write 32 bit value to memory. */
MsgWrite64 = 7 MsgWrite64 = 7, /**< Write 64 bit value to memory. */
MsgMultiCommand = 0xFF /**< Treats multiple IPC commands in batch. */
}; };
// possible result codes
enum IPCResult /**
* IPC message buffer.
* A list of all needed fields to store an IPC message.
*/
struct IPCBuffer
{ {
IPC_OK = 0, int size; /**< Size of the buffer. */
IPC_FAIL = 0xFF char* buffer; /**< Buffer. */
};
/**
* IPC result codes.
* A list of possible result codes the IPC can send back.
* Each one of them is what we call an "opcode" or "tag" and is the
* first byte sent by the IPC to differentiate between results.
*/
enum IPCResult : unsigned char
{
IPC_OK = 0, /**< IPC command successfully completed. */
IPC_FAIL = 0xFF /**< IPC command failed to complete. */
}; };
// handle to the main vm thread // handle to the main vm thread
@ -79,9 +119,9 @@ protected:
/* Internal function, Parses an IPC command. /* Internal function, Parses an IPC command.
* buf: buffer containing the IPC command. * buf: buffer containing the IPC command.
* ret_buffer: buffer that will be used to send the reply. * ret_buffer: buffer that will be used to send the reply.
* return value: pair containing a buffer with the result * return value: IPCBuffer containing a buffer with the result
* of the command and its size. */ * of the command and its size. */
std::pair<int, char*> ParseCommand(char* buf, char* ret_buffer); IPCBuffer ParseCommand(char* buf, char* ret_buffer);
/* Formats an IPC buffer /* Formats an IPC buffer
* ret_buffer: return buffer to use. * ret_buffer: return buffer to use.