/* 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 . */ #pragma once #include "types.h" #ifdef NAOMI_MULTIBOARD #include "naomi_regs.h" #include "hw/holly/sb.h" #include "hw/sh4/sh4_mem.h" #include #ifdef _WIN32 #include #include 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::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& 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& lock) { waitMs(lock, INFINITE); } template std::cv_status wait_for(std::unique_lock& lock, const std::chrono::duration& rel_time) { return waitMs(lock, std::chrono::duration_cast(rel_time).count()); } IpcConditionVariable& operator=(const IpcConditionVariable&) = delete; }; #else // _!WIN32 #include 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& lock) { pthread_cond_wait(&cond, lock.mutex()->native_handle()); } template std::cv_status wait_for(std::unique_lock& lock, const std::chrono::duration& rel_time) { timespec ts; clock_gettime(CLOCK_REALTIME, &ts); std::chrono::seconds seconds = std::chrono::duration_cast(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 status; IpcMutex mutex; IpcConditionVariable cond; std::atomic boardReady[4]; std::atomic boardSynced[4]; std::atomic 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