diff --git a/src/mips/psyqo/cdrom-device.hh b/src/mips/psyqo/cdrom-device.hh index 96dfc6b38..36fd00f0f 100644 --- a/src/mips/psyqo/cdrom-device.hh +++ b/src/mips/psyqo/cdrom-device.hh @@ -39,6 +39,8 @@ SOFTWARE. namespace psyqo { +class GPU; + namespace Concepts { template @@ -58,6 +60,13 @@ concept IsCDRomDeviceStateEnum = * * @details This class is a specialization of the CDRom interface, which * provides a way to read from the physical CDRom drive of the console. + * All of the methods in this class are asynchronous, and will call the + * provided callback when the operation is complete. The class also + * provides a blocking variant for some of the methods, which can be + * used to perform the operation synchronously. Note that the blocking + * variants are only provided for methods that are expected to complete + * quickly, and should not be used in performance-critical code, as they + * can still block the system for several milliseconds. * */ class CDRomDevice final : public CDRom { @@ -124,6 +133,19 @@ class CDRomDevice final : public CDRom { void readSectors(uint32_t sector, uint32_t count, void *buffer, eastl::function &&callback) override; TaskQueue::Task scheduleReadSectors(uint32_t sector, uint32_t count, void *buffer); + /** + * @brief Gets the size of the Table of Contents from the CDRom. Note that + * while the blocking variant is available because it is a fairly short + * operation with the CDRom controller, it can still block the system + * for roughly 2ms, which is a long time in the context of a 33MHz CPU. + * + * @param size The pointer to store the size of the TOC. + * @param callback The callback to call when the size is retrieved. + */ + void getTOCSize(unsigned *size, eastl::function &&callback); + TaskQueue::Task scheduleGetTOCSize(unsigned *size); + unsigned getTOCSizeBlocking(GPU &); + /** * @brief Reads the Table of Contents from the CDRom. * @@ -133,14 +155,52 @@ class CDRomDevice final : public CDRom { * provided buffer starting at index 1 for the first track. Any tracks * that are not present on the CD will not have their MSF structure * filled in, so the application should ensure that the buffer is - * initialized to zero before calling this method. + * initialized to zero before calling this method. The blocking variant + * may take a total of 200ms to complete, depending on the number of + * tracks on the CD. * - * @param toc The buffer to read the TOC into. Ideally, this buffer - * should be able to hold 100 `MSF` structures for safety. + * @param toc The buffer to read the TOC into. + * @param size The size of the buffer. Should be 100 to hold all possible tracks. * @param callback The callback to call when the read is complete. */ - void readTOC(MSF *toc, eastl::function &&callback); - TaskQueue::Task scheduleReadTOC(MSF *toc); + void readTOC(MSF *toc, unsigned size, eastl::function &&callback); + TaskQueue::Task scheduleReadTOC(MSF *toc, unsigned size); + bool readTOCBlocking(MSF *toc, unsigned size, GPU &); + + /** + * @brief Mutes the CD audio for both CDDA and CDXA. + * + * @param callback The callback to call when the mute operation is complete. + */ + void mute(eastl::function &&callback); + TaskQueue::Task scheduleMute(); + void muteBlocking(GPU &); + + /** + * @brief Unmutes the CD audio for both CDDA and CDXA. + * + * @param callback The callback to call when the unmute operation is complete. + */ + void unmute(eastl::function &&callback); + TaskQueue::Task scheduleUnmute(); + void unmuteBlocking(GPU &); + + /** + * @brief Begins playing CDDA audio from a given starting point. + * + * @details This method will begin playing CDDA audio from a given + * starting point. The starting point is either a track number or + * an MSF value. The callback will be called when playback is complete, + * paused, or if an error occurs, which can be after the end of the + * track if `stopAtEndOfTrack` is set to true, or at the end of the + * disc if the last track is reached. + * + * @param start The starting point for playback. + * @param stopAtEndOfTrack If true, playback will stop at the end of the track. + * @param callback The callback to call when playback is complete. + */ + void playCDDA(MSF start, bool stopAtEndOfTrack, eastl::function &&callback); + void playCDDA(unsigned track, bool stopAtEndOfTrack, eastl::function &&callback); /** * @brief The action base class for the internal state machine. @@ -167,6 +227,7 @@ class CDRomDevice final : public CDRom { private: void switchAction(ActionBase *action); void irq(); + void actionComplete(); friend class ActionBase; @@ -175,6 +236,16 @@ class CDRomDevice final : public CDRom { ActionBase *m_action = nullptr; uint8_t m_state = 0; bool m_success = false; + bool m_blocking = false; + + struct BlockingAction { + BlockingAction(CDRomDevice *, GPU &); + ~BlockingAction(); + + private: + CDRomDevice *m_device; + GPU &m_gpu; + }; }; } // namespace psyqo diff --git a/src/mips/psyqo/examples/task-demo/task-demo.cpp b/src/mips/psyqo/examples/task-demo/task-demo.cpp index 6047fcf00..89715448d 100644 --- a/src/mips/psyqo/examples/task-demo/task-demo.cpp +++ b/src/mips/psyqo/examples/task-demo/task-demo.cpp @@ -113,11 +113,12 @@ void TaskDemo::createScene() { .then([this](auto task) { m_text = "Success!"; syscall_puts("Success!\n"); + ramsyscall_printf("Track count: %d\n", m_cdrom.getTOCSizeBlocking(gpu())); m_systemCnfSize = m_request.entry.size; m_done = true; task->resolve(); }) - .then(m_cdrom.scheduleReadTOC(m_toc)) + .then(m_cdrom.scheduleReadTOC(m_toc, 100)) .then([this](auto task) { for (unsigned i = 1; i < 100; i++) { if (m_toc[i].m == 0 && m_toc[i].s == 0 && m_toc[i].f == 0) { diff --git a/src/mips/psyqo/hardware/cdrom.hh b/src/mips/psyqo/hardware/cdrom.hh index e875180db..d26014e14 100644 --- a/src/mips/psyqo/hardware/cdrom.hh +++ b/src/mips/psyqo/hardware/cdrom.hh @@ -43,7 +43,7 @@ enum class CDL : uint8_t { PAUSE = 9, INIT = 10, MUTE = 11, - DEMUTE = 12, + UNMUTE = 12, SETFILTER = 13, SETMODE = 14, GETMODE = 15, diff --git a/src/mips/psyqo/hardware/cpu.hh b/src/mips/psyqo/hardware/cpu.hh index c831408ab..81ade298a 100644 --- a/src/mips/psyqo/hardware/cpu.hh +++ b/src/mips/psyqo/hardware/cpu.hh @@ -49,6 +49,7 @@ struct IRQReg : public Register { void set(IRQ irq) { *this |= (static_cast(irq)); } void clear(IRQ irq) { *this &= ~(static_cast(irq)); } void clear() { Register::access() = 0; } + bool isSet(IRQ irq) const { return (*this & static_cast(irq)) != 0; } }; extern IRQReg<0x0070> IReg; diff --git a/src/mips/psyqo/src/cdrom-device.cpp b/src/mips/psyqo/src/cdrom-device.cpp index 07d8cedd6..d0dab9757 100644 --- a/src/mips/psyqo/src/cdrom-device.cpp +++ b/src/mips/psyqo/src/cdrom-device.cpp @@ -31,6 +31,7 @@ SOFTWARE. #include "common/hardware/dma.h" #include "common/kernel/events.h" #include "common/syscalls/syscalls.h" +#include "psyqo/gpu.hh" #include "psyqo/hardware/cdrom.hh" #include "psyqo/hardware/cpu.hh" #include "psyqo/hardware/sbus.hh" @@ -89,13 +90,13 @@ class ResetAction : public psyqo::CDRomDevice::Action { } }; -ResetAction resetAction; +ResetAction s_resetAction; } // namespace void psyqo::CDRomDevice::reset(eastl::function &&callback) { Kernel::assert(m_callback == nullptr, "CDRomDevice::reset called with pending action"); - resetAction.start(this, eastl::move(callback)); + s_resetAction.start(this, eastl::move(callback)); } psyqo::TaskQueue::Task psyqo::CDRomDevice::scheduleReset() { @@ -186,24 +187,88 @@ class ReadSectorsAction : public psyqo::CDRomDevice::Action &&callback) { Kernel::assert(m_callback == nullptr, "CDRomDevice::readSectors called with pending action"); - readSectorsAction.start(this, sector, count, buffer, eastl::move(callback)); + s_readSectorsAction.start(this, sector, count, buffer, eastl::move(callback)); } psyqo::TaskQueue::Task psyqo::CDRomDevice::scheduleReadSectors(uint32_t sector, uint32_t count, void *buffer) { - return TaskQueue::Task([this, sector, count, buffer](auto task) { + if (count == 0) { + return TaskQueue::Task([this](auto task) { task->complete(true); }); + } + uint32_t *storage = reinterpret_cast(buffer); + storage[0] = sector; + storage[1] = count; + return TaskQueue::Task([this, buffer](auto task) { + uint32_t *storage = reinterpret_cast(buffer); + uint32_t sector = storage[0]; + uint32_t count = storage[1]; readSectors(sector, count, buffer, [task](bool success) { task->complete(success); }); }); } namespace { +enum class GetTNActionEnum : uint8_t { + IDLE, + GETTN, +}; + +class GetTNAction : public psyqo::CDRomDevice::Action { + public: + void start(psyqo::CDRomDevice *device, unsigned *size, eastl::function &&callback) { + psyqo::Kernel::assert(getState() == GetTNActionEnum::IDLE, + "CDRomDevice::getTOCSize() called while another action is in progress"); + registerMe(device); + setCallback(eastl::move(callback)); + setState(GetTNActionEnum::GETTN); + m_size = size; + eastl::atomic_signal_fence(eastl::memory_order_release); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::GETTN); + } + bool acknowledge(const psyqo::CDRomDevice::Response &response) override { + *m_size = response[2]; + setSuccess(true); + return true; + } + + private: + unsigned *m_size = nullptr; +}; + +GetTNAction s_getTNAction; + +} // namespace + +void psyqo::CDRomDevice::getTOCSize(unsigned *size, eastl::function &&callback) { + Kernel::assert(m_callback == nullptr, "CDRomDevice::getTOCSize called with pending action"); + s_getTNAction.start(this, size, eastl::move(callback)); +} + +psyqo::TaskQueue::Task psyqo::CDRomDevice::scheduleGetTOCSize(unsigned *size) { + return TaskQueue::Task( + [this, size](auto task) { getTOCSize(size, [task](bool success) { task->complete(success); }); }); +} + +unsigned psyqo::CDRomDevice::getTOCSizeBlocking(GPU &gpu) { + Kernel::assert(m_callback == nullptr, "CDRomDevice::getTOCSizeBlocking called with pending action"); + unsigned size = 0; + bool success = false; + { + BlockingAction blocking(this, gpu); + s_getTNAction.start(this, &size, [&success](bool success_) { success = success_; }); + } + if (!success) return 0; + return size; +} + +namespace { + enum class ReadTOCActionState : uint8_t { IDLE, GETTN, @@ -212,13 +277,14 @@ enum class ReadTOCActionState : uint8_t { class ReadTOCAction : public psyqo::CDRomDevice::Action { public: - void start(psyqo::CDRomDevice *device, psyqo::MSF *toc, eastl::function &&callback) { + void start(psyqo::CDRomDevice *device, psyqo::MSF *toc, unsigned size, eastl::function &&callback) { psyqo::Kernel::assert(getState() == ReadTOCActionState::IDLE, "CDRomDevice::readTOC() called while another action is in progress"); registerMe(device); setCallback(eastl::move(callback)); - m_toc = toc; setState(ReadTOCActionState::GETTN); + m_toc = toc; + m_size = size; eastl::atomic_signal_fence(eastl::memory_order_release); psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::GETTN); } @@ -235,7 +301,8 @@ class ReadTOCAction : public psyqo::CDRomDevice::Action { msf.m = psyqo::btoi(response[1]); msf.s = psyqo::btoi(response[2]); msf.f = 0; - if (++m_currentTrack <= m_lastTrack) { + m_currentTrack++; + if ((m_currentTrack <= m_lastTrack) && (m_currentTrack < m_size)) { psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::GETTD, psyqo::itob(m_currentTrack)); } else { @@ -250,23 +317,204 @@ class ReadTOCAction : public psyqo::CDRomDevice::Action { return false; } psyqo::MSF *m_toc = nullptr; + unsigned m_size = 0; uint8_t m_currentTrack = 0; uint8_t m_lastTrack = 0; }; -ReadTOCAction readTOCAction; +ReadTOCAction s_readTOCAction; } // namespace -void psyqo::CDRomDevice::readTOC(MSF *toc, eastl::function &&callback) { +void psyqo::CDRomDevice::readTOC(MSF *toc, unsigned size, eastl::function &&callback) { Kernel::assert(m_callback == nullptr, "CDRomDevice::readTOC called with pending action"); - readTOCAction.start(this, toc, eastl::move(callback)); + s_readTOCAction.start(this, toc, size, eastl::move(callback)); +} + +psyqo::TaskQueue::Task psyqo::CDRomDevice::scheduleReadTOC(MSF *toc, unsigned size) { + if (size == 0) { + return TaskQueue::Task([this](auto task) { task->complete(true); }); + } + size = eastl::min(size, 100u); + toc[0].m = size; + return TaskQueue::Task([this, toc](auto task) { + unsigned size = toc[0].m; + toc[0].m = 0; + readTOC(toc, size, [task](bool success) { task->complete(success); }); + }); } -psyqo::TaskQueue::Task psyqo::CDRomDevice::scheduleReadTOC(MSF *toc) { - return TaskQueue::Task([this, toc](auto task) { readTOC(toc, [task](bool success) { task->complete(success); }); }); +bool psyqo::CDRomDevice::readTOCBlocking(MSF *toc, unsigned size, GPU &gpu) { + Kernel::assert(m_callback == nullptr, "CDRomDevice::readTOCBlocking called with pending action"); + bool success = false; + { + BlockingAction blocking(this, gpu); + readTOC(toc, size, [&success](bool success_) { success = success_; }); + } + return success; } +namespace { + +enum class MuteActionState : uint8_t { + IDLE, + MUTE, +}; + +class MuteAction : public psyqo::CDRomDevice::Action { + public: + void start(psyqo::CDRomDevice *device, eastl::function &&callback) { + psyqo::Kernel::assert(getState() == MuteActionState::IDLE, + "CDRomDevice::mute() called while another action is in progress"); + registerMe(device); + setCallback(eastl::move(callback)); + setState(MuteActionState::MUTE); + eastl::atomic_signal_fence(eastl::memory_order_release); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::MUTE); + } + bool complete(const psyqo::CDRomDevice::Response &) override { + setSuccess(true); + return true; + } +}; + +MuteAction s_muteAction; + +} // namespace + +void psyqo::CDRomDevice::mute(eastl::function &&callback) { + Kernel::assert(m_callback == nullptr, "CDRomDevice::mute called with pending action"); + s_muteAction.start(this, eastl::move(callback)); +} + +psyqo::TaskQueue::Task psyqo::CDRomDevice::scheduleMute() { + return TaskQueue::Task([this](auto task) { mute([task](bool success) { task->complete(success); }); }); +} + +namespace { + +enum class UnmuteActionState : uint8_t { + IDLE, + UNMUTE, +}; + +class UnmuteAction : public psyqo::CDRomDevice::Action { + public: + void start(psyqo::CDRomDevice *device, eastl::function &&callback) { + psyqo::Kernel::assert(getState() == UnmuteActionState::IDLE, + "CDRomDevice::unmute() called while another action is in progress"); + registerMe(device); + setCallback(eastl::move(callback)); + setState(UnmuteActionState::UNMUTE); + eastl::atomic_signal_fence(eastl::memory_order_release); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::UNMUTE); + } + bool complete(const psyqo::CDRomDevice::Response &) override { + setSuccess(true); + return true; + } +}; + +UnmuteAction s_unmuteAction; + +} // namespace + +void psyqo::CDRomDevice::unmute(eastl::function &&callback) { + Kernel::assert(m_callback == nullptr, "CDRomDevice::unmute called with pending action"); + s_unmuteAction.start(this, eastl::move(callback)); +} + +psyqo::TaskQueue::Task psyqo::CDRomDevice::scheduleUnmute() { + return TaskQueue::Task([this](auto task) { unmute([task](bool success) { task->complete(success); }); }); +} + +namespace { + +enum class PlayCDDAActionState : uint8_t { + IDLE, + GETTD, + SETMODE, + SETLOC, + SEEK, + SEEK_ACK, + PLAY, + PLAYING, +}; + +class PlayCDDAAction : public psyqo::CDRomDevice::Action { + public: + void start(psyqo::CDRomDevice *device, unsigned track, bool stopAtEndOfTrack, + eastl::function &&callback) { + psyqo::Kernel::assert(getState() == PlayCDDAActionState::IDLE, + "CDRomDevice::playCDDA() called while another action is in progress"); + registerMe(device); + setCallback(eastl::move(callback)); + setState(PlayCDDAActionState::GETTD); + m_stopAtEndOfTrack = stopAtEndOfTrack; + eastl::atomic_signal_fence(eastl::memory_order_release); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::GETTD, psyqo::itob(track)); + } + void start(psyqo::CDRomDevice *device, psyqo::MSF msf, bool stopAtEndOfTrack, + eastl::function &&callback) { + psyqo::Kernel::assert(getState() == PlayCDDAActionState::IDLE, + "CDRomDevice::playCDDA() called while another action is in progress"); + registerMe(device); + setCallback(eastl::move(callback)); + setState(PlayCDDAActionState::SEEK); + m_start = msf; + eastl::atomic_signal_fence(eastl::memory_order_release); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::SETMODE, stopAtEndOfTrack ? 0x02 : 0); + } + bool complete(const psyqo::CDRomDevice::Response &) override { + psyqo::Kernel::assert(getState() == PlayCDDAActionState::SEEK_ACK, + "PlayCDDAAction got CDROM complete in wrong state"); + setState(PlayCDDAActionState::PLAY); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::PLAY); + return false; + } + bool acknowledge(const psyqo::CDRomDevice::Response &response) override { + switch (getState()) { + case PlayCDDAActionState::GETTD: + m_start.m = psyqo::btoi(response[1]); + m_start.s = psyqo::btoi(response[2]); + m_start.f = 0; + setState(PlayCDDAActionState::SETMODE); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::SETMODE, + m_stopAtEndOfTrack ? 0x02 : 0); + break; + case PlayCDDAActionState::SETMODE: + setState(PlayCDDAActionState::SETLOC); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::SETLOC, m_start.m, m_start.s, + m_start.f); + break; + case PlayCDDAActionState::SETLOC: + setState(PlayCDDAActionState::SEEK); + psyqo::Hardware::CDRom::Command.send(psyqo::Hardware::CDRom::CDL::SEEKP); + break; + case PlayCDDAActionState::SEEK: + setState(PlayCDDAActionState::SEEK_ACK); + break; + case PlayCDDAActionState::PLAY: + setState(PlayCDDAActionState::PLAYING); + break; + default: + psyqo::Kernel::abort("PlayCDDAAction got CDROM acknowledge in wrong state"); + break; + } + return false; + } + bool end(const psyqo::CDRomDevice::Response &) override { + psyqo::Kernel::assert(getState() == PlayCDDAActionState::PLAYING, + "PlayCDDAAction got CDROM end in wrong state"); + setSuccess(true); + return true; + } + psyqo::MSF m_start; + bool m_stopAtEndOfTrack = false; +}; + +} // namespace + void psyqo::CDRomDevice::switchAction(ActionBase *action) { Kernel::assert(m_action == nullptr, "CDRomDevice can only have one action active at a given time"); m_action = action; @@ -315,17 +563,43 @@ void psyqo::CDRomDevice::irq() { if (callCallback) { Kernel::assert(!!m_callback, "Wrong CDRomDevice state"); m_action = nullptr; - eastl::atomic_signal_fence(eastl::memory_order_acquire); - Kernel::queueCallbackFromISR([this]() { - auto callback = eastl::move(m_callback); - auto success = m_success; - m_success = false; - m_state = 0; - callback(success); - }); + if (m_blocking) { + actionComplete(); + } else { + eastl::atomic_signal_fence(eastl::memory_order_acquire); + Kernel::queueCallbackFromISR([this]() { actionComplete(); }); + } } } +psyqo::CDRomDevice::BlockingAction::BlockingAction(CDRomDevice *device, GPU &gpu) : m_device(device), m_gpu(gpu) { + device->m_blocking = true; + Hardware::CPU::IMask.clear(Hardware::CPU::IRQ::CDRom); +} + +psyqo::CDRomDevice::BlockingAction::~BlockingAction() { + auto device = m_device; + auto gpu = &m_gpu; + while (device->m_state != 0) { + if (Hardware::CPU::IReg.isSet(Hardware::CPU::IRQ::CDRom)) { + Hardware::CPU::IReg.clear(Hardware::CPU::IRQ::CDRom); + device->irq(); + } + gpu->pumpCallbacks(); + } + device->m_blocking = false; + Hardware::CPU::IMask.set(Hardware::CPU::IRQ::CDRom); +} + +void psyqo::CDRomDevice::actionComplete() { + auto callback = eastl::move(m_callback); + m_callback = nullptr; + auto success = m_success; + m_success = false; + m_state = 0; + callback(success); +} + void psyqo::CDRomDevice::ActionBase::setCallback(eastl::function &&callback) { auto &deviceCallback = m_device->m_callback; Kernel::assert(!deviceCallback && m_device->m_state == 0, "Action setup called with pending action");