diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b763dff..3dc7e467 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,7 +197,7 @@ set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selecto src/core/applets/error_applet.cpp ) set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp src/core/audio/teakra_core.cpp - src/core/audio/miniaudio_device.cpp + src/core/audio/miniaudio_device.cpp src/core/audio/hle_core.cpp ) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) @@ -234,6 +234,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp + include/audio/hle_core.hpp ) cmrc_add_resource_library( diff --git a/include/audio/dsp_core.hpp b/include/audio/dsp_core.hpp index 1a556f28..a4fb1ab1 100644 --- a/include/audio/dsp_core.hpp +++ b/include/audio/dsp_core.hpp @@ -37,7 +37,7 @@ namespace Audio { MAKE_LOG_FUNCTION(log, dspLogger) public: - enum class Type { Null, Teakra }; + enum class Type { Null, Teakra, HLE }; DSPCore(Memory& mem, Scheduler& scheduler, DSPService& dspService) : mem(mem), scheduler(scheduler), dspService(dspService) {} virtual ~DSPCore() {} diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp new file mode 100644 index 00000000..6d7b3ad1 --- /dev/null +++ b/include/audio/hle_core.hpp @@ -0,0 +1,157 @@ +#pragma once +#include +#include +#include + +#include "audio/dsp_core.hpp" +#include "audio/dsp_shared_mem.hpp" +#include "memory.hpp" + +namespace Audio { + using SampleFormat = HLE::SourceConfiguration::Configuration::Format; + using SourceType = HLE::SourceConfiguration::Configuration::MonoOrStereo; + + struct DSPSource { + // Audio buffer information + // https://www.3dbrew.org/wiki/DSP_Memory_Region + struct Buffer { + u32 paddr; // Physical address of the buffer + u32 sampleCount; // Total number of samples + u8 adpcmScale; // ADPCM predictor/scale + u8 pad1; // Unknown + + std::array previousSamples; // ADPCM y[n-1] and y[n-2] + bool adpcmDirty; + bool looping; + u16 bufferID; + u8 pad2; + + u32 playPosition = 0; // Current position in the buffer + SampleFormat format; + SourceType sourceType; + + bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? + bool hasPlayedOnce = false; // Has the buffer been played at least once before? + + bool operator<(const Buffer& other) const { + // Lower ID = Higher priority + // If this buffer ID is greater than the other one, then this buffer has a lower priority + return this->bufferID > other.bufferID; + } + }; + + using BufferQueue = std::priority_queue; + + std::array gain0, gain1, gain2; + u16 syncCount; + bool enabled; + + BufferQueue buffers; + int index = 0; // Index of the voice in [0, 23] for debugging + + void reset(); + DSPSource() { reset(); } + }; + + class HLE_DSP : public DSPCore { + // The audio frame types are public in case we want to use them for unit tests + public: + template + using Sample = std::array; + + template + using Frame = std::array, 160>; + + template + using MonoFrame = Frame; + + template + using StereoFrame = Frame; + + template + using QuadFrame = Frame; + + using Source = Audio::DSPSource; + private: + enum class DSPState : u32 { + Off, + On, + Slep, + }; + + // Number of DSP pipes + static constexpr size_t pipeCount = 8; + DSPState dspState; + + std::array, pipeCount> pipeData; // The data of each pipe + std::array sources; // DSP voices + Audio::HLE::DspMemory dspRam; + + SampleFormat sampleFormat = SampleFormat::ADPCM; + SourceType sourceType = SourceType::Stereo; + + void resetAudioPipe(); + bool loaded = false; // Have we loaded a component? + + // Get the index for the current region we'll be reading. Returns the region with the highest frame counter + // Accounting for whether one of the frame counters has wrapped around + usize readRegionIndex() const { + const auto counter0 = dspRam.region0.frameCounter; + const auto counter1 = dspRam.region1.frameCounter; + + // Handle wraparound cases first + if (counter0 == 0xffff && counter1 != 0xfffe) { + return 1; + } else if (counter1 == 0xffff && counter0 != 0xfffe) { + return 0; + } else { + return counter0 > counter1 ? 0 : 0; + } + } + + // DSP shared memory is double buffered; One region is being written to while the other one is being read from + Audio::HLE::SharedMemory& readRegion() { return readRegionIndex() == 0 ? dspRam.region0 : dspRam.region1; } + Audio::HLE::SharedMemory& writeRegion() { return readRegionIndex() == 0 ? dspRam.region1 : dspRam.region0; } + + // Get a pointer of type T* to the data starting from physical address paddr + template + T* getPointerPhys(u32 paddr, u32 size = 0) { + if (paddr >= PhysicalAddrs::FCRAM && paddr + size <= PhysicalAddrs::FCRAMEnd) { + u8* fcram = mem.getFCRAM(); + u32 index = paddr - PhysicalAddrs::FCRAM; + + return (T*)&fcram[index]; + } else if (paddr >= PhysicalAddrs::DSP_RAM && paddr + size <= PhysicalAddrs::DSP_RAM_End) { + u32 index = paddr - PhysicalAddrs::DSP_RAM; + return (T*)&dspRam.rawMemory[index]; + } else [[unlikely]] { + Helpers::warn("[DSP] Tried to access unknown physical address: %08X", paddr); + return nullptr; + } + } + + void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config); + void generateFrame(StereoFrame& frame); + void outputFrame(); + void dumpBuffer(const Source::Buffer& buffer); + public: + HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService); + ~HLE_DSP() override {} + + void reset() override; + void runAudioFrame() override; + + u8* getDspMemory() override { return dspRam.rawMemory.data(); } + + u16 recvData(u32 regId) override; + bool recvDataIsReady(u32 regId) override { return true; } // Treat data as always ready + void writeProcessPipe(u32 channel, u32 size, u32 buffer) override; + std::vector readPipe(u32 channel, u32 peer, u32 size, u32 buffer) override; + + void loadComponent(std::vector& data, u32 programMask, u32 dataMask) override; + void unloadComponent() override; + void setSemaphore(u16 value) override {} + void setSemaphoreMask(u16 value) override {} + }; + +} // namespace Audio \ No newline at end of file diff --git a/include/memory.hpp b/include/memory.hpp index 1b6e622c..33ccbae5 100644 --- a/include/memory.hpp +++ b/include/memory.hpp @@ -19,7 +19,10 @@ namespace PhysicalAddrs { VRAM = 0x18000000, VRAMEnd = VRAM + 0x005FFFFF, FCRAM = 0x20000000, - FCRAMEnd = FCRAM + 0x07FFFFFF + FCRAMEnd = FCRAM + 0x07FFFFFF, + + DSP_RAM = 0x1FF00000, + DSP_RAM_End = DSP_RAM + 0x0007FFFF }; } diff --git a/src/core/audio/dsp_core.cpp b/src/core/audio/dsp_core.cpp index e4162e93..01cee11e 100644 --- a/src/core/audio/dsp_core.cpp +++ b/src/core/audio/dsp_core.cpp @@ -1,18 +1,20 @@ #include "audio/dsp_core.hpp" -#include "audio/null_core.hpp" -#include "audio/teakra_core.hpp" - #include #include #include +#include "audio/hle_core.hpp" +#include "audio/null_core.hpp" +#include "audio/teakra_core.hpp" + std::unique_ptr Audio::makeDSPCore(DSPCore::Type type, Memory& mem, Scheduler& scheduler, DSPService& dspService) { std::unique_ptr core; switch (type) { case DSPCore::Type::Null: core = std::make_unique(mem, scheduler, dspService); break; case DSPCore::Type::Teakra: core = std::make_unique(mem, scheduler, dspService); break; + case DSPCore::Type::HLE: core = std::make_unique(mem, scheduler, dspService); break; default: Helpers::warn("Invalid DSP core selected!"); @@ -29,10 +31,8 @@ Audio::DSPCore::Type Audio::DSPCore::typeFromString(std::string inString) { std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); }); static const std::unordered_map map = { - {"null", Audio::DSPCore::Type::Null}, - {"none", Audio::DSPCore::Type::Null}, - {"lle", Audio::DSPCore::Type::Teakra}, - {"teakra", Audio::DSPCore::Type::Teakra}, + {"null", Audio::DSPCore::Type::Null}, {"none", Audio::DSPCore::Type::Null}, {"lle", Audio::DSPCore::Type::Teakra}, + {"teakra", Audio::DSPCore::Type::Teakra}, {"hle", Audio::DSPCore::Type::HLE}, {"fast", Audio::DSPCore::Type::HLE}, }; if (auto search = map.find(inString); search != map.end()) { @@ -47,6 +47,7 @@ const char* Audio::DSPCore::typeToString(Audio::DSPCore::Type type) { switch (type) { case Audio::DSPCore::Type::Null: return "null"; case Audio::DSPCore::Type::Teakra: return "teakra"; + case Audio::DSPCore::Type::HLE: return "hle"; default: return "invalid"; } } diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp new file mode 100644 index 00000000..245894ce --- /dev/null +++ b/src/core/audio/hle_core.cpp @@ -0,0 +1,311 @@ +#include "audio/hle_core.hpp" + +#include +#include + +#include "services/dsp.hpp" + +namespace Audio { + namespace DSPPipeType { + enum : u32 { + Debug = 0, + DMA = 1, + Audio = 2, + Binary = 3, + }; + } + + HLE_DSP::HLE_DSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) : DSPCore(mem, scheduler, dspService) { + // Set up source indices + for (int i = 0; i < sources.size(); i++) { + sources[i].index = i; + } + } + + void HLE_DSP::resetAudioPipe() { +#define DSPOffset(var) (0x8000 + offsetof(Audio::HLE::SharedMemory, var) / 2) + + // These are DSP shared memory offsets for various variables + // https://www.3dbrew.org/wiki/DSP_Memory_Region + static constexpr std::array responses = { + 0x000F, // Number of responses + DSPOffset(frameCounter), // Frame counter + DSPOffset(sourceConfigurations), // Source configs + DSPOffset(sourceStatuses), // Source statuses + DSPOffset(adpcmCoefficients), // ADPCM coefficients + DSPOffset(dspConfiguration), // DSP configs + DSPOffset(dspStatus), // DSP status + DSPOffset(finalSamples), // Final samples + DSPOffset(intermediateMixSamples), // Intermediate mix samples + DSPOffset(compressor), // Compressor + DSPOffset(dspDebug), // Debug + DSPOffset(unknown10), // ?? + DSPOffset(unknown11), // ?? + DSPOffset(unknown12), // ?? + DSPOffset(unknown13), // Surround sound biquad filter 1 + DSPOffset(unknown14) // Surround sound biquad filter 2 + }; +#undef DSPOffset + + std::vector& audioPipe = pipeData[DSPPipeType::Audio]; + audioPipe.resize(responses.size() * sizeof(u16)); + + // Push back every response to the audio pipe + size_t index = 0; + for (auto e : responses) { + audioPipe[index++] = e & 0xff; + audioPipe[index++] = e >> 8; + } + } + + void HLE_DSP::reset() { + dspState = DSPState::Off; + loaded = false; + + // Initialize these to some sane defaults + sampleFormat = SampleFormat::ADPCM; + sourceType = SourceType::Stereo; + + for (auto& e : pipeData) { + e.clear(); + } + + for (auto& source : sources) { + source.reset(); + } + + // Note: Reset audio pipe AFTER resetting all pipes, otherwise the new data will be yeeted + resetAudioPipe(); + } + + void HLE_DSP::loadComponent(std::vector& data, u32 programMask, u32 dataMask) { + if (loaded) { + Helpers::warn("Loading DSP component when already loaded"); + } + + loaded = true; + scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); + } + + void HLE_DSP::unloadComponent() { + if (!loaded) { + Helpers::warn("Audio: unloadComponent called without a running program"); + } + + loaded = false; + scheduler.removeEvent(Scheduler::EventType::RunDSP); + } + + void HLE_DSP::runAudioFrame() { + // Signal audio pipe when an audio frame is done + if (dspState == DSPState::On) [[likely]] { + dspService.triggerPipeEvent(DSPPipeType::Audio); + } + + outputFrame(); + scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); + } + + u16 HLE_DSP::recvData(u32 regId) { + if (regId != 0) { + Helpers::panic("Audio: invalid register in HLE frontend"); + } + + return dspState == DSPState::On; + } + + void HLE_DSP::writeProcessPipe(u32 channel, u32 size, u32 buffer) { + enum class StateChange : u8 { + Initialize = 0, + Shutdown = 1, + Wakeup = 2, + Sleep = 3, + }; + + switch (channel) { + case DSPPipeType::Audio: { + if (size != 4) { + printf("Invalid size written to DSP Audio Pipe\n"); + break; + } + + // Get new state + const u8 state = mem.read8(buffer); + if (state > 3) { + log("WriteProcessPipe::Audio: Unknown state change type"); + } else { + switch (static_cast(state)) { + case StateChange::Initialize: + // TODO: Other initialization stuff here + dspState = DSPState::On; + resetAudioPipe(); + + dspService.triggerPipeEvent(DSPPipeType::Audio); + break; + + case StateChange::Shutdown: + dspState = DSPState::Off; + break; + + default: Helpers::panic("Unimplemented DSP audio pipe state change %d", state); + } + } + break; + } + + case DSPPipeType::Binary: + Helpers::warn("Unimplemented write to binary pipe! Size: %d\n", size); + + // This pipe and interrupt are normally used for requests like AAC decode + dspService.triggerPipeEvent(DSPPipeType::Binary); + break; + + default: log("Audio::HLE_DSP: Wrote to unimplemented pipe %d\n", channel); break; + } + } + + std::vector HLE_DSP::readPipe(u32 pipe, u32 peer, u32 size, u32 buffer) { + if (size & 1) Helpers::panic("Tried to read odd amount of bytes from DSP pipe"); + if (pipe >= pipeCount || size > 0xffff) { + return {}; + } + + if (pipe != DSPPipeType::Audio) { + log("Reading from non-audio pipe! This might be broken, might need to check what pipe is being read from and implement writing to it\n"); + } + + std::vector& data = pipeData[pipe]; + size = std::min(size, data.size()); // Clamp size to the maximum available data size + + if (size == 0) { + return {}; + } + + // Return "size" bytes from the audio pipe and erase them + std::vector out(data.begin(), data.begin() + size); + data.erase(data.begin(), data.begin() + size); + return out; + } + + void HLE_DSP::outputFrame() { + StereoFrame frame; + generateFrame(frame); + + if (audioEnabled) { + // Wait until we've actually got room to push our frame + while (sampleBuffer.size() + 2 > sampleBuffer.Capacity()) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } + + sampleBuffer.push(frame.data(), frame.size()); + } + } + + void HLE_DSP::generateFrame(StereoFrame& frame) { + using namespace Audio::HLE; + SharedMemory& read = readRegion(); + SharedMemory& write = writeRegion(); + + for (int i = 0; i < sourceCount; i++) { + // Update source configuration from the read region of shared memory + auto& config = read.sourceConfigurations.config[i]; + auto& source = sources[i]; + updateSourceConfig(source, config); + + // Generate audio + if (source.enabled && !source.buffers.empty()) { + const auto& buffer = source.buffers.top(); + const u8* data = getPointerPhys(buffer.paddr); + + if (data != nullptr) { + // TODO + } + } + + // Update write region of shared memory + auto& status = write.sourceStatuses.status[i]; + status.isEnabled = source.enabled; + status.syncCount = source.syncCount; + } + } + + void HLE_DSP::updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config) { + // Check if the any dirty bit is set, otherwise exit early + if (!config.dirtyRaw) { + return; + } + + if (config.enableDirty) { + config.enableDirty = 0; + source.enabled = config.enable != 0; + } + + if (config.syncCountDirty) { + config.syncCountDirty = 0; + source.syncCount = config.syncCount; + } + + if (config.resetFlag) { + config.resetFlag = 0; + source.reset(); + } + + if (config.partialResetFlag) { + config.partialResetFlag = 0; + source.buffers = {}; + } + + // TODO: Should we check bufferQueueDirty here too? + if (config.formatDirty || config.embeddedBufferDirty) { + sampleFormat = config.format; + } + + if (config.monoOrStereoDirty || config.embeddedBufferDirty) { + sourceType = config.monoOrStereo; + } + + if (config.embeddedBufferDirty) { + config.embeddedBufferDirty = 0; + if (s32(config.length) >= 0) [[likely]] { + // TODO: Add sample format and channel count + Source::Buffer buffer{ + .paddr = config.physicalAddress, + .sampleCount = config.length, + .adpcmScale = u8(config.adpcm_ps), + .previousSamples = {s16(config.adpcm_yn[0]), s16(config.adpcm_yn[1])}, + .adpcmDirty = config.adpcmDirty != 0, + .looping = config.isLooping != 0, + .bufferID = config.bufferID, + .playPosition = config.playPosition, + .format = sampleFormat, + .sourceType = sourceType, + .fromQueue = false, + .hasPlayedOnce = false, + }; + + source.buffers.emplace(std::move(buffer)); + } else { + log("Invalid embedded buffer size for DSP voice %d\n", source.index); + } + } + + if (config.partialEmbeddedBufferDirty) { + config.partialEmbeddedBufferDirty = 0; + printf("Partial embedded buffer dirty for voice %d\n", source.index); + } + + if (config.bufferQueueDirty) { + config.bufferQueueDirty = 0; + printf("Buffer queue dirty for voice %d\n", source.index); + } + + config.dirtyRaw = 0; + } + + void DSPSource::reset() { + enabled = false; + syncCount = 0; + + buffers = {}; + } +} // namespace Audio