flycast/core/hw/naomi/multiboard.h

587 lines
13 KiB
C++

/*
Copyright 2023 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/>.
*/
#pragma once
#include "types.h"
#ifdef NAOMI_MULTIBOARD
#include "naomi_regs.h"
#include "hw/holly/sb.h"
#include "hw/sh4/sh4_mem.h"
#include <atomic>
#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
class IpcMutex
{
HANDLE mutex;
public:
using native_handle_type = HANDLE;
IpcMutex()
{
SECURITY_ATTRIBUTES secattr{ sizeof(SECURITY_ATTRIBUTES) };
secattr.bInheritHandle = TRUE;
mutex = CreateMutex(&secattr, FALSE, NULL);
if (mutex == NULL)
throw std::runtime_error("CreateMutex failed");
}
IpcMutex(const IpcMutex&) = delete;
~IpcMutex() {
CloseHandle(mutex);
}
void lock() {
WaitForSingleObject(mutex, INFINITE);
}
void unlock() {
ReleaseMutex(mutex);
}
native_handle_type native_handle() {
return mutex;
}
IpcMutex& operator=(const IpcMutex&) = delete;
};
class IpcConditionVariable
{
class Semaphore
{
HANDLE handle;
public:
Semaphore()
{
SECURITY_ATTRIBUTES secattr{ sizeof(SECURITY_ATTRIBUTES) };
secattr.bInheritHandle = TRUE;
handle = CreateSemaphore(&secattr, 0, std::numeric_limits<LONG>::max(), NULL);
if (handle == NULL)
throw std::runtime_error("Semaphore create failed");
}
~Semaphore() {
CloseHandle(handle);
}
bool wait(DWORD msecs = INFINITE)
{
DWORD rc = WaitForSingleObject(handle, msecs);
if (rc == WAIT_ABANDONED || rc == WAIT_FAILED)
throw std::runtime_error("Semaphore wait failure");
return rc != WAIT_TIMEOUT;
}
void signal(int n = 1) {
ReleaseSemaphore(handle, n, nullptr);
}
};
Semaphore semaphore;
IpcMutex mutex;
int waiters = 0;
// The unlock/wait/lock in wait() should be atomic so the h semaphore is used
// to make sure all processes about to wait (i.e. on the waiters list) are notified before any other.
// This race condition happens more frequently with one slave, where one process
// notifies the other that everybody is ready and ends up notifying itself,
// while the other process waits indefinitely.
// See https://birrell.org/andrew/papers/ImplementingCVs.pdf
Semaphore h;
std::cv_status waitMs(std::unique_lock<IpcMutex>& lock, DWORD msecs)
{
mutex.lock();
waiters++;
mutex.unlock();
lock.unlock();
std::cv_status status = semaphore.wait(msecs) ?
std::cv_status::no_timeout : std::cv_status::timeout;
// must be done before re-acquiring the lock
h.signal();
lock.lock();
return status;
}
public:
void notify_all()
{
mutex.lock();
if (waiters > 0)
{
semaphore.signal(waiters);
// make sure waiters are notified before moving on
do {
h.wait();
waiters--;
} while (waiters > 0);
}
mutex.unlock();
}
void wait(std::unique_lock<IpcMutex>& lock)
{
waitMs(lock, INFINITE);
}
template<class Rep, class Period>
std::cv_status wait_for(std::unique_lock<IpcMutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time)
{
return waitMs(lock, std::chrono::duration_cast<std::chrono::milliseconds>(rel_time).count());
}
IpcConditionVariable& operator=(const IpcConditionVariable&) = delete;
};
#else // _!WIN32
#include <pthread.h>
class IpcMutex
{
pthread_mutex_t mutex;
public:
using native_handle_type = pthread_mutex_t*;
IpcMutex()
{
pthread_mutexattr_t mattr;
pthread_mutexattr_init(&mattr);
pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&mutex, &mattr);
pthread_mutexattr_destroy(&mattr);
}
IpcMutex(const IpcMutex&) = delete;
~IpcMutex() {
pthread_mutex_destroy(&mutex);
}
void lock() {
pthread_mutex_lock(&mutex);
}
void unlock() {
pthread_mutex_unlock(&mutex);
}
native_handle_type native_handle() {
return &mutex;
}
IpcMutex& operator=(const IpcMutex&) = delete;
};
class IpcConditionVariable
{
pthread_cond_t cond;
public:
IpcConditionVariable()
{
pthread_condattr_t cvattr;
pthread_condattr_init(&cvattr);
pthread_condattr_setpshared(&cvattr, PTHREAD_PROCESS_SHARED);
pthread_cond_init(&cond, &cvattr);
pthread_condattr_destroy(&cvattr);
}
IpcConditionVariable(const IpcConditionVariable&) = delete;
~IpcConditionVariable() {
pthread_cond_destroy(&cond);
}
void notify_all() {
pthread_cond_broadcast(&cond);
}
void wait(std::unique_lock<IpcMutex>& lock) {
pthread_cond_wait(&cond, lock.mutex()->native_handle());
}
template<class Rep, class Period>
std::cv_status wait_for(std::unique_lock<IpcMutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time)
{
timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
std::chrono::seconds seconds = std::chrono::duration_cast<std::chrono::seconds>(rel_time);
ts.tv_sec += seconds.count();
auto nanoTime = rel_time - seconds;
ts.tv_nsec += std::chrono::nanoseconds(nanoTime).count();
if (ts.tv_nsec >= 1000000000) {
ts.tv_sec++;
ts.tv_nsec -= 1000000000;
}
int rc = pthread_cond_timedwait(&cond, lock.mutex()->native_handle(), &ts);
return rc == ETIMEDOUT ? std::cv_status::timeout : std::cv_status::no_timeout;
}
IpcConditionVariable& operator=(const IpcConditionVariable&) = delete;
};
#endif // _!WIN32
class Multiboard
{
public:
static constexpr u32 G1_BASE = 0x05F7080;
static constexpr u32 G2_BASE = 0x1010000;
Multiboard();
~Multiboard();
u32 readG1(u32 addr, u32 size)
{
switch (addr)
{
case NAOMI_MBOARD_OFFSET_addr:
return offset;
case NAOMI_MBOARD_DATA_addr:
{
u32 bank = (sharedMem->status & 1) ? 0 : 0x80000;
u32 addr;
if (isMaster())
{
bank = 0x80000 - bank;
addr = offset + bank / 2;
}
else
{
addr = offset + (boardId - 1) * 0x10000 + bank / 2;
}
u16 data = sharedMem->data[addr & (MEM_SIZE - 1)];
DEBUG_LOG(NAOMI, "read NAOMI_COMM_DATA[%x]: %x (pc = %x)", addr, data, p_sh4rcb->cntx.pc);
offset++;
return data;
}
case 0x5f7074:
DEBUG_LOG(NAOMI, "5F7074 read: %d", reg74); // loops from 0 to ff
return reg74 & 0xff;
case 0x5f706C:
DEBUG_LOG(NAOMI, "5F706C read");
return 0; // written to C4 afterwards & 7 except if == 7
case G1_BASE + 0x08:
DEBUG_LOG(NAOMI, "5F7088 read");
return 0x80; // loops until bit 7 is set
case G1_BASE + 0x10:
DEBUG_LOG(NAOMI, "5F7090 read");
return 0x60; // ? or 0x61 or 0x62
case G1_BASE + 0x14:
DEBUG_LOG(NAOMI, "5F7094 read");
return 0x43; // set to 43 before
default:
DEBUG_LOG(NAOMI, "Unknown G1 register read<%d>: %x (pc = %x)", size, addr, p_sh4rcb->cntx.pc);
return 0xFFFF;
}
}
void writeG1(u32 addr, u32 size, u32 data)
{
switch (addr)
{
case NAOMI_MBOARD_OFFSET_addr:
DEBUG_LOG(NAOMI, "NAOMI_COMM_OFFSET = %x (pc = %x)", data, p_sh4rcb->cntx.pc);
offset = data;
break;
case NAOMI_MBOARD_DATA_addr:
{
DEBUG_LOG(NAOMI, "NAOMI_COMM_DATA[%x] = %x (pc = °%x)", offset, data, p_sh4rcb->cntx.pc);
u32 bank = (sharedMem->status & 1) ? 0 : 0x80000;
u32 addr;
if (isMaster())
{
bank = 0x80000 - bank;
addr = offset + bank / 2;
}
else
{
addr = offset + (boardId - 1) * 0x10000 + bank / 2;
}
sharedMem->data[addr & (MEM_SIZE - 1)] = data;
offset++;
}
break;
case 0x5f7070:
DEBUG_LOG(NAOMI, "5F7070 written: %d", data);
break;
case 0x5f7074:
DEBUG_LOG(NAOMI, "5F7074 written: %d", data);
reg74 = data;
startSlave();
break;
case 0x5f7058: // Set to 0 before DMA operation from multiboard
break;
case NAOMI_MBOARD_STATUS_addr:
// Set to 4 after writing most packets
if (isSlave())
{
if ((data & 4) != 0)
sharedMem->status.fetch_or(0x10 << boardId);
//else
// sharedMem->status.fetch_and(~(0x10 << boardId));
}
break;
default:
DEBUG_LOG(NAOMI, "Unknown G1 register written<%d>: %x = %x (pc = %x)", size, addr, data, p_sh4rcb->cntx.pc);
break;
}
}
u32 readG2Ext(u32 addr, u32 size)
{
//DEBUG_LOG(NAOMI, "g2ext_readMem<%d> %x (pc = %x)", size, addr, p_sh4rcb->cntx.pc);
switch (addr)
{
case G2_BASE + 0x08:
return 0x80; // loops until bit 7 is set
case G2_BASE + 0x10: // similar to 5F7090
return 0x60;
case G2_BASE + 0x14: // similar to 5F7094
return 0x43;
case G2_BASE + 0x94:
return 0; // ?
case G2_BASE + 0x98:
return 0; // ?
case G2_BASE + 0x9c: // similar to 5F7074
return isMaster() ? reg9c : 0;
case G2_BASE + 0xc0: // similar to 5F706C. need to match!
return 0;
case 0x1008000: // status reg
{
verify(size == 2);
// seems to determine if acting as master or slave
if (isSlave())
return 0;
u16 v = 0xff00 | sharedMem->status;
DEBUG_LOG(NAOMI, "g2ext_readMem status_reg %x", v);
return v;
}
default:
if ((addr - 0x1020000) < MEM_SIZE * 2)
{
u32 bank = (sharedMem->status & 1) ? 0x80000 : 0;
DEBUG_LOG(NAOMI, "g2ext_readMem<%d> %x -> %x", size, addr, sharedMem->data[(addr - 0x1020000 + bank) / 2]);
verify(size >= 2);
u32 offset;
if (isSlave())
{
return 0;
bank = 0x80000 - bank;
if (addr >= 0x1040000) {
INFO_LOG(NAOMI, "Read shared mem out of bound for slave: %x", addr);
break;
}
offset = (addr - 0x1020000 + bank + (boardId - 1) * 0x20000) / 2;
}
else
offset = (addr - 0x1020000 + bank) / 2;
if (size == 2)
return sharedMem->data[offset];
else
return *(u32 *)&sharedMem->data[offset];
}
break;
}
return 0;
}
void writeG2Ext(u32 addr, u32 size, u32 data)
{
//DEBUG_LOG(NAOMI, "g2ext_writeMem<%d> %x = %x", size, addr, data);
switch (addr) {
case 0x1008000: // status reg
verify(size == 2);
if (isMaster())
sharedMem->status = data;
DEBUG_LOG(NAOMI, "g2ext_writeMem status_reg %x", data);
break;
case G2_BASE + 0x9c:
reg9c = data;
break;
case G2_BASE + 0xa0: // LEDs
DEBUG_LOG(NAOMI, "G2 leds %x", data);
break;
default:
if ((addr - 0x1020000) < MEM_SIZE * 2)
{
DEBUG_LOG(NAOMI, "g2ext_writeMem<%d> %x: %x", size, addr, data);
verify(size >= 2);
u32 bank = (sharedMem->status & 1) ? 0x80000 : 0;
if (isSlave())
{
break;
bank = 0x80000 - bank;
if (addr >= 0x1040000) {
INFO_LOG(NAOMI, "Write shared mem out of bound for slave: %x", addr);
break;
}
u32 offset = (addr - 0x1020000 + bank + (boardId - 1) * 0x20000) / 2;
if (size == 2)
sharedMem->data[offset] = data;
else
*(u32 *)&sharedMem->data[offset] = data;
}
else
{
u32 offset = (addr - 0x1020000 + bank) / 2;
if ((sharedMem->status & 2) != 0 || addr >= 0x1040000)
{
if (size == 2)
sharedMem->data[offset] = data;
else
*(u32 *)&sharedMem->data[offset] = data;
}
if (addr < 0x1040000) // FIXME this is weird
{
if ((sharedMem->status & 4) != 0)
{
if (size == 2)
sharedMem->data[offset + 0x10000] = data;
else
*(u32 *)&sharedMem->data[offset + 0x10000] = data;
}
if (sharedMem->status & 8)
{
if (size == 2)
sharedMem->data[offset + 0x20000] = data;
else
*(u32 *)&sharedMem->data[offset + 0x20000] = data;
}
}
}
}
break;
}
}
bool dmaStart()
{
if (isMaster())
return false;
DEBUG_LOG(NAOMI, "Multiboard DMA start addr %08X len %d", SB_GDSTAR, SB_GDLEN);
verify(1 == SB_GDDIR);
u32 start = SB_GDSTAR & 0x1FFFFFE0;
u32 len = (SB_GDLEN + 31) & ~31;
u32 bank = (sharedMem->status & 1) ? 0 : 0x80000;
u32 *src = (u32 *)&sharedMem->data[(boardId - 1) * 0x10000 + bank / 2];
WriteMemBlock_nommu_ptr(start, src, len);
SB_GDSTARD = start + len;
SB_GDLEND = len;
return true;
}
void reset()
{
if (isSlave())
sharedMem->status.fetch_and(~(0x10 << boardId));
}
void syncWait();
private:
bool isMaster() const { return boardId == 0; }
bool isSlave() const { return boardId != 0; }
void startSlave();
static constexpr size_t MEM_SIZE = 0x100000 / sizeof(u16);
struct SharedMemory
{
std::atomic<u16> status;
IpcMutex mutex;
IpcConditionVariable cond;
std::atomic<bool> boardReady[4];
std::atomic<bool> boardSynced[4];
std::atomic<bool> exit;
u16 data[MEM_SIZE];
};
int boardId = 0;
u32 offset = 0;
u16 reg74 = 0;
u32 reg9c = 0;
SharedMemory *sharedMem;
#ifdef _WIN32
HANDLE mapFile;
#else
std::string sharedMemFileName;
#endif
int boardCount = 0;
bool slaveStarted = false;
int schedId;
};
#else // !NAOMI_MULTIBOARD
class Multiboard
{
public:
u32 readG1(u32 addr, u32 size) {
return 0;
}
void writeG1(u32 addr, u32 size, u32 data) { }
u32 readG2Ext(u32 addr, u32 size) {
return 0;
}
void writeG2Ext(u32 addr, u32 size, u32 data) { }
bool dmaStart() {
return false;
}
void reset() { }
void syncWait() { }
};
#endif // !NAOMI_MULTIBOARD