mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-27 09:05:28 -04:00
ee149581aa
* feat: implement memory card IOP * feat: IOP trace * feat: move IOP logic to ps2xIOP refactor: small refactor on audio api on runtime
629 lines
24 KiB
C++
629 lines
24 KiB
C++
#include "../module_factories.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <limits>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <string_view>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
|
|
namespace ps2x::iop::detail
|
|
{
|
|
namespace
|
|
{
|
|
constexpr uint32_t kCommandSid = 0x00000000u;
|
|
constexpr uint32_t kStateSid = 0x00000001u;
|
|
constexpr uint32_t kSubmitFunction = 0x00000000u;
|
|
constexpr uint32_t kGetStatusAddressFunction = 0x00000012u;
|
|
constexpr uint32_t kGetAddressTableFunction = 0x00000013u;
|
|
|
|
constexpr uint32_t kStatusSize = 0x42u;
|
|
constexpr uint32_t kSeInfoOffset = 0x00u;
|
|
constexpr uint32_t kMidiInfoOffset = 0x0Cu;
|
|
constexpr uint32_t kMidiSumOffset = 0x1Eu;
|
|
constexpr uint32_t kSeSumOffset = 0x26u;
|
|
constexpr uint32_t kAddressTableEntries = 16u;
|
|
constexpr uint32_t alignUp(uint32_t value, uint32_t alignment)
|
|
{
|
|
if (alignment == 0u)
|
|
{
|
|
return value;
|
|
}
|
|
return (value + (alignment - 1u)) & ~(alignment - 1u);
|
|
}
|
|
|
|
template <typename T>
|
|
bool readGuestPod(const IopHost &host, uint32_t address, T &value)
|
|
{
|
|
value = {};
|
|
return host.readGuest(address, &value, sizeof(value));
|
|
}
|
|
|
|
template <typename T>
|
|
bool writeGuestPod(IopHost &host, uint32_t address, const T &value)
|
|
{
|
|
return host.writeGuest(address, &value, sizeof(value));
|
|
}
|
|
|
|
template <typename T, size_t Size>
|
|
bool hasAnyNonZero(const std::array<T, Size> &values)
|
|
{
|
|
return std::any_of(values.begin(), values.end(), [](const T value)
|
|
{ return value != static_cast<T>(0); });
|
|
}
|
|
|
|
size_t commandLength(uint8_t command)
|
|
{
|
|
const uint8_t hi = static_cast<uint8_t>(command & 0xF0u);
|
|
switch (hi)
|
|
{
|
|
case 0x00u:
|
|
{
|
|
size_t length = 4u;
|
|
if ((command & 0x01u) != 0u)
|
|
{
|
|
++length;
|
|
}
|
|
if ((command & 0x02u) != 0u)
|
|
{
|
|
++length;
|
|
}
|
|
if ((command & 0x04u) != 0u)
|
|
{
|
|
length += 2u;
|
|
}
|
|
return length;
|
|
}
|
|
case 0x10u:
|
|
return command == 0x11u ? 3u : 1u;
|
|
case 0x20u:
|
|
if (command == 0x22u || command == 0x23u || command == 0x24u || command == 0x25u)
|
|
{
|
|
return 3u;
|
|
}
|
|
if (command == 0x26u)
|
|
{
|
|
return 4u;
|
|
}
|
|
if (command == 0x20u)
|
|
{
|
|
return 5u;
|
|
}
|
|
if (command == 0x27u || command == 0x28u || command == 0x29u ||
|
|
command == 0x2Cu || command == 0x2Du)
|
|
{
|
|
return 8u;
|
|
}
|
|
return 2u;
|
|
case 0x40u:
|
|
if (command == 0x47u || command == 0x48u || command == 0x49u || command == 0x4Au ||
|
|
command == 0x41u || command == 0x42u)
|
|
{
|
|
return 2u;
|
|
}
|
|
if (command == 0x4Bu)
|
|
{
|
|
return 3u;
|
|
}
|
|
if (command == 0x45u || command == 0x4Cu)
|
|
{
|
|
return 4u;
|
|
}
|
|
if (command == 0x44u)
|
|
{
|
|
return 6u;
|
|
}
|
|
if (command == 0x4Du || command == 0x4Eu)
|
|
{
|
|
return 3u;
|
|
}
|
|
if (command == 0x4Fu)
|
|
{
|
|
return 6u;
|
|
}
|
|
return 1u;
|
|
case 0x50u:
|
|
case 0x60u:
|
|
if (command == 0x51u || command == 0x52u || command == 0x53u || command == 0x54u)
|
|
{
|
|
return 8u;
|
|
}
|
|
return 2u;
|
|
default:
|
|
return 0u;
|
|
}
|
|
}
|
|
|
|
class TsnddrvService final : public IopService
|
|
{
|
|
public:
|
|
TsnddrvService(IopHost &host, TsnddrvBindings bindings)
|
|
: m_host(host), m_bindings(std::move(bindings))
|
|
{
|
|
}
|
|
|
|
[[nodiscard]] std::string_view name() const override
|
|
{
|
|
return m_bindings.serviceName;
|
|
}
|
|
|
|
[[nodiscard]] std::span<const uint32_t> sids() const override
|
|
{
|
|
return m_sids;
|
|
}
|
|
|
|
void reset() override
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
m_state = {};
|
|
}
|
|
|
|
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
|
{
|
|
RpcResult result{};
|
|
|
|
if (request.sid == kCommandSid && request.function == kSubmitFunction)
|
|
{
|
|
handleCommandBuffer(request.send);
|
|
result.handled = true;
|
|
}
|
|
else if (request.sid == kStateSid &&
|
|
(request.function == kGetStatusAddressFunction ||
|
|
request.function == kGetAddressTableFunction))
|
|
{
|
|
uint32_t responseAddress = 0u;
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (!ensureMemoryLocked())
|
|
{
|
|
return result;
|
|
}
|
|
responseAddress = request.function == kGetStatusAddressFunction
|
|
? m_state.statusAddress
|
|
: m_state.addressTableAddress;
|
|
}
|
|
|
|
if (request.receive.address != 0u && request.receive.size >= sizeof(uint32_t))
|
|
{
|
|
(void)writeGuestPod(m_host, request.receive.address, responseAddress);
|
|
if (request.receive.size > sizeof(uint32_t))
|
|
{
|
|
(void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t),
|
|
request.receive.size - sizeof(uint32_t));
|
|
}
|
|
result.resultAddress = request.receive.address;
|
|
}
|
|
|
|
result.handled = true;
|
|
result.signalNowaitCompletion = true;
|
|
}
|
|
|
|
if (result.handled)
|
|
{
|
|
const auto rule = std::find_if(
|
|
m_bindings.completionRules.begin(),
|
|
m_bindings.completionRules.end(),
|
|
[&](const TsnddrvCompletionRule &candidate) {
|
|
return candidate.eeFunction == request.endFunction;
|
|
});
|
|
if (rule != m_bindings.completionRules.end())
|
|
{
|
|
if (rule->suppressGuestCallback)
|
|
{
|
|
result.callbackPolicy = CallbackPolicy::Suppress;
|
|
}
|
|
result.signalCompletion = rule->signalCompletion;
|
|
if (rule->clearBusy)
|
|
{
|
|
constexpr uint32_t idle = 0u;
|
|
(void)writeGuestPod(m_host,
|
|
m_bindings.busyFlagAddress,
|
|
idle);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void onSifTransfer(const SifTransfer &transfer) override
|
|
{
|
|
if (transfer.kind != SifTransferKind::GetOtherData ||
|
|
transfer.phase != SifTransferPhase::BeforeCopy ||
|
|
transfer.size != kStatusSize)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (!m_state.initialized || transfer.sourceAddress != m_state.statusAddress)
|
|
{
|
|
return;
|
|
}
|
|
backfillStatusLocked();
|
|
}
|
|
|
|
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
metrics.push_back({"initialized", m_state.initialized ? 1u : 0u, false});
|
|
metrics.push_back({"status_address", m_state.statusAddress, true});
|
|
metrics.push_back({"address_table", m_state.addressTableAddress, true});
|
|
metrics.push_back({"hd_base", m_state.hdBaseAddress, true});
|
|
metrics.push_back({"sq_base", m_state.sqBaseAddress, true});
|
|
metrics.push_back({"data_base", m_state.dataBaseAddress, true});
|
|
}
|
|
|
|
private:
|
|
struct State
|
|
{
|
|
bool initialized = false;
|
|
uint32_t storageBaseAddress = 0u;
|
|
uint32_t storageSize = 0u;
|
|
uint32_t statusAddress = 0u;
|
|
uint32_t addressTableAddress = 0u;
|
|
uint32_t hdBaseAddress = 0u;
|
|
uint32_t sqBaseAddress = 0u;
|
|
uint32_t dataBaseAddress = 0u;
|
|
};
|
|
|
|
bool ensureMemoryLocked()
|
|
{
|
|
if (m_state.statusAddress == 0u)
|
|
{
|
|
const TsnddrvGuestArena &arena = m_bindings.arena;
|
|
const uint32_t statusAddress = alignUp(arena.base, arena.statusAlignment);
|
|
const uint32_t addressTableAddress =
|
|
alignUp(statusAddress + kStatusSize, arena.tableAlignment);
|
|
const uint32_t hdBaseAddress =
|
|
alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)),
|
|
arena.storageAlignment);
|
|
const uint32_t sqBaseAddress =
|
|
alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment);
|
|
const uint32_t dataBaseAddress =
|
|
alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment);
|
|
const uint32_t storageEnd = dataBaseAddress + arena.dataBytes;
|
|
if (storageEnd > arena.limit)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
m_state.statusAddress = statusAddress;
|
|
m_state.addressTableAddress = addressTableAddress;
|
|
m_state.hdBaseAddress = hdBaseAddress;
|
|
m_state.sqBaseAddress = sqBaseAddress;
|
|
m_state.dataBaseAddress = dataBaseAddress;
|
|
m_state.storageBaseAddress = hdBaseAddress;
|
|
m_state.storageSize = storageEnd - hdBaseAddress;
|
|
}
|
|
|
|
if (m_state.statusAddress == 0u ||
|
|
m_state.addressTableAddress == 0u ||
|
|
m_state.storageBaseAddress == 0u)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!m_state.initialized)
|
|
{
|
|
if (!m_host.zeroGuest(m_state.statusAddress, kStatusSize) ||
|
|
!m_host.zeroGuest(m_state.addressTableAddress,
|
|
kAddressTableEntries * sizeof(uint32_t)) ||
|
|
!m_host.zeroGuest(m_state.storageBaseAddress, m_state.storageSize))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!writeGuestPod(m_host,
|
|
m_state.addressTableAddress + (0u * sizeof(uint32_t)),
|
|
m_state.hdBaseAddress) ||
|
|
!writeGuestPod(m_host,
|
|
m_state.addressTableAddress + (1u * sizeof(uint32_t)),
|
|
m_state.sqBaseAddress) ||
|
|
!writeGuestPod(m_host,
|
|
m_state.addressTableAddress + (2u * sizeof(uint32_t)),
|
|
m_state.dataBaseAddress))
|
|
{
|
|
return false;
|
|
}
|
|
m_state.initialized = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int16_t checkValue(bool seTable,
|
|
uint32_t index,
|
|
uint32_t count) const
|
|
{
|
|
if (index >= count)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates)
|
|
{
|
|
const uint32_t base = seTable ? candidate.seAddress : candidate.midiAddress;
|
|
int16_t value = 0;
|
|
if (readGuestPod(m_host,
|
|
base + (index * sizeof(int16_t)),
|
|
value) &&
|
|
value != 0)
|
|
{
|
|
return value;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool selectCompatChecks(uint32_t &seBase, uint32_t &midiBase) const
|
|
{
|
|
const TsnddrvChecksumTables *firstReadable = nullptr;
|
|
for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates)
|
|
{
|
|
std::array<int16_t, 5> seValues{};
|
|
std::array<int16_t, 4> midiValues{};
|
|
const bool seReadable = m_host.readGuest(candidate.seAddress,
|
|
seValues.data(),
|
|
sizeof(seValues));
|
|
const bool midiReadable = m_host.readGuest(candidate.midiAddress,
|
|
midiValues.data(),
|
|
sizeof(midiValues));
|
|
if (seReadable && midiReadable && !firstReadable)
|
|
{
|
|
firstReadable = &candidate;
|
|
}
|
|
const bool looksLive =
|
|
(seReadable && hasAnyNonZero(seValues)) ||
|
|
(midiReadable && hasAnyNonZero(midiValues));
|
|
if (seReadable && midiReadable && looksLive)
|
|
{
|
|
seBase = candidate.seAddress;
|
|
midiBase = candidate.midiAddress;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (firstReadable)
|
|
{
|
|
seBase = firstReadable->seAddress;
|
|
midiBase = firstReadable->midiAddress;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void backfillStatusLocked()
|
|
{
|
|
uint32_t seBase = 0u;
|
|
uint32_t midiBase = 0u;
|
|
if (!selectCompatChecks(seBase, midiBase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto backfillSlots = [&](uint32_t statusOffset,
|
|
uint32_t compatBase,
|
|
uint32_t slotCount)
|
|
{
|
|
for (uint32_t slot = 0u; slot < slotCount; ++slot)
|
|
{
|
|
int16_t liveValue = 0;
|
|
if (!readGuestPod(m_host,
|
|
m_state.statusAddress + statusOffset +
|
|
(slot * sizeof(int16_t)),
|
|
liveValue) ||
|
|
liveValue != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int16_t compatValue = 0;
|
|
if (!readGuestPod(m_host,
|
|
compatBase + (slot * sizeof(int16_t)),
|
|
compatValue) ||
|
|
compatValue == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
(void)writeGuestPod(m_host,
|
|
m_state.statusAddress + statusOffset +
|
|
(slot * sizeof(int16_t)),
|
|
compatValue);
|
|
}
|
|
};
|
|
|
|
backfillSlots(kSeSumOffset, seBase, 5u);
|
|
backfillSlots(kMidiSumOffset, midiBase, 4u);
|
|
}
|
|
|
|
void applyCommandLocked(const std::array<uint8_t, 8> &command)
|
|
{
|
|
if (m_state.statusAddress == 0u)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (command[0])
|
|
{
|
|
case 0x20u: // SdrBgmReq
|
|
{
|
|
const uint32_t port = command[1] & 0x0Fu;
|
|
uint16_t midiInfo = 0u;
|
|
(void)readGuestPod(m_host,
|
|
m_state.statusAddress + kMidiInfoOffset,
|
|
midiInfo);
|
|
midiInfo = static_cast<uint16_t>(midiInfo |
|
|
static_cast<uint16_t>(1u << port));
|
|
(void)writeGuestPod(m_host,
|
|
m_state.statusAddress + kMidiInfoOffset,
|
|
midiInfo);
|
|
break;
|
|
}
|
|
case 0x21u: // SdrBgmStop
|
|
{
|
|
const uint32_t port = command[1] & 0x0Fu;
|
|
uint16_t midiInfo = 0u;
|
|
(void)readGuestPod(m_host,
|
|
m_state.statusAddress + kMidiInfoOffset,
|
|
midiInfo);
|
|
midiInfo = static_cast<uint16_t>(midiInfo &
|
|
~static_cast<uint16_t>(1u << port));
|
|
(void)writeGuestPod(m_host,
|
|
m_state.statusAddress + kMidiInfoOffset,
|
|
midiInfo);
|
|
break;
|
|
}
|
|
case 0x28u: // SdrHDDataSet
|
|
{
|
|
const uint32_t port = command[1] & 0x0Fu;
|
|
if (port >= 4u)
|
|
{
|
|
break;
|
|
}
|
|
const int16_t checksum = checkValue(false, port, 4u);
|
|
(void)writeGuestPod(m_host,
|
|
m_state.statusAddress + kMidiSumOffset +
|
|
(port * sizeof(int16_t)),
|
|
checksum);
|
|
break;
|
|
}
|
|
case 0x29u: // SdrHDDataSet2
|
|
{
|
|
const uint32_t port = command[1] & 0x0Fu;
|
|
if (port >= 5u)
|
|
{
|
|
break;
|
|
}
|
|
const int16_t checksum = checkValue(true, port, 5u);
|
|
(void)writeGuestPod(m_host,
|
|
m_state.statusAddress + kSeSumOffset +
|
|
(port * sizeof(int16_t)),
|
|
checksum);
|
|
break;
|
|
}
|
|
case 0x10u: // SdrSeAllStop
|
|
(void)m_host.zeroGuest(m_state.statusAddress + kSeInfoOffset,
|
|
6u * sizeof(uint16_t));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void handleCommandBuffer(GuestBuffer send)
|
|
{
|
|
if (send.address == 0u || send.size == 0u)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(m_mutex);
|
|
if (!ensureMemoryLocked())
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (uint32_t offset = 0u; offset < send.size;)
|
|
{
|
|
uint8_t operation = 0u;
|
|
if (!m_host.readGuest(send.address + offset, &operation, sizeof(operation)) ||
|
|
operation == 0xFFu)
|
|
{
|
|
break;
|
|
}
|
|
|
|
const size_t length = commandLength(operation);
|
|
if (length == 0u ||
|
|
static_cast<uint64_t>(offset) + length > send.size)
|
|
{
|
|
break;
|
|
}
|
|
|
|
std::array<uint8_t, 8> command{};
|
|
if (!m_host.readGuest(send.address + offset, command.data(), length))
|
|
{
|
|
break;
|
|
}
|
|
applyCommandLocked(command);
|
|
offset += static_cast<uint32_t>(length);
|
|
}
|
|
}
|
|
|
|
IopHost &m_host;
|
|
TsnddrvBindings m_bindings;
|
|
mutable std::mutex m_mutex;
|
|
State m_state;
|
|
const std::array<uint32_t, 2> m_sids = {kCommandSid, kStateSid};
|
|
};
|
|
}
|
|
|
|
std::unique_ptr<IopService> createTsnddrvService(IopHost &host,
|
|
TsnddrvBindings bindings)
|
|
{
|
|
const auto isPowerOfTwo = [](uint32_t value) {
|
|
return value != 0u && (value & (value - 1u)) == 0u;
|
|
};
|
|
const auto alignUp64 = [](uint64_t value, uint32_t alignment) {
|
|
return (value + (alignment - 1u)) &
|
|
~static_cast<uint64_t>(alignment - 1u);
|
|
};
|
|
|
|
const TsnddrvGuestArena &arena = bindings.arena;
|
|
if (bindings.serviceName.empty() ||
|
|
arena.base >= arena.limit ||
|
|
!isPowerOfTwo(arena.statusAlignment) ||
|
|
!isPowerOfTwo(arena.tableAlignment) ||
|
|
!isPowerOfTwo(arena.storageAlignment) ||
|
|
arena.hdBytes == 0u || arena.sqBytes == 0u || arena.dataBytes == 0u ||
|
|
bindings.checksumCandidates.empty())
|
|
{
|
|
throw std::invalid_argument("invalid TSNDDRV bindings");
|
|
}
|
|
|
|
uint64_t end = alignUp64(arena.base, arena.statusAlignment) + kStatusSize;
|
|
end = alignUp64(end, arena.tableAlignment) +
|
|
(kAddressTableEntries * sizeof(uint32_t));
|
|
end = alignUp64(end, arena.storageAlignment) + arena.hdBytes;
|
|
end = alignUp64(end, arena.storageAlignment) + arena.sqBytes;
|
|
end = alignUp64(end, arena.storageAlignment) + arena.dataBytes;
|
|
if (end > arena.limit || end > std::numeric_limits<uint32_t>::max())
|
|
{
|
|
throw std::invalid_argument("TSNDDRV guest arena is too small");
|
|
}
|
|
|
|
for (const TsnddrvChecksumTables &candidate : bindings.checksumCandidates)
|
|
{
|
|
if (candidate.seAddress == 0u || candidate.midiAddress == 0u)
|
|
{
|
|
throw std::invalid_argument("incomplete TSNDDRV checksum binding");
|
|
}
|
|
}
|
|
|
|
std::unordered_set<uint32_t> callbacks;
|
|
for (const TsnddrvCompletionRule &rule : bindings.completionRules)
|
|
{
|
|
if (rule.eeFunction == 0u || !callbacks.emplace(rule.eeFunction).second ||
|
|
(rule.clearBusy && bindings.busyFlagAddress == 0u))
|
|
{
|
|
throw std::invalid_argument("invalid TSNDDRV completion rule");
|
|
}
|
|
}
|
|
|
|
switch (bindings.protocol)
|
|
{
|
|
case TsnddrvProtocolVariant::SndQueueV1:
|
|
break;
|
|
}
|
|
return std::make_unique<TsnddrvService>(host, std::move(bindings));
|
|
}
|
|
}
|