diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml index 137577c18..2d0fd844d 100644 --- a/.github/workflows/Android_Build.yml +++ b/.github/workflows/Android_Build.yml @@ -45,8 +45,11 @@ jobs: git apply ./.github/gles.patch # Build the project with CMake cmake --build ${{github.workspace}}/build --config ${{ env.BUILD_TYPE }} - # Move the generated library to the appropriate location + + # Strip the generated library and move it to the appropriate location + ${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --strip-unneeded ./build/libAlber.so mv ./build/libAlber.so ./src/pandroid/app/src/main/jniLibs/x86_64/ + # Build the Android app with Gradle cd src/pandroid ./gradlew assemble${{ env.BUILD_TYPE }} @@ -97,8 +100,11 @@ jobs: git apply ./.github/gles.patch # Build the project with CMake cmake --build ${{github.workspace}}/build --config ${{ env.BUILD_TYPE }} - # Move the generated library to the appropriate location + + # Strip the generated library and move it to the appropriate location + ${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip --strip-unneeded ./build/libAlber.so mv ./build/libAlber.so ./src/pandroid/app/src/main/jniLibs/arm64-v8a/ + # Build the Android app with Gradle cd src/pandroid ./gradlew assemble${{ env.BUILD_TYPE }} diff --git a/.gitmodules b/.gitmodules index f1e8f469c..77a6bc6a4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -55,3 +55,9 @@ [submodule "third_party/libuv"] path = third_party/libuv url = https://github.com/libuv/libuv +[submodule "third_party/miniaudio"] + path = third_party/miniaudio + url = https://github.com/mackron/miniaudio +[submodule "third_party/teakra"] + path = third_party/teakra + url = https://github.com/wwylele/teakra diff --git a/CMakeLists.txt b/CMakeLists.txt index 456d6513a..4dbe438f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ include_directories(third_party/xxhash/include) include_directories(third_party/httplib) include_directories(third_party/stb) include_directories(third_party/opengl) +include_directories(third_party/miniaudio) include_directories(third_party/mio/single_include) add_compile_definitions(NOMINMAX) # Make windows.h not define min/max macros because third-party deps don't like it @@ -149,12 +150,13 @@ if(HOST_X64 OR HOST_ARM64) else() message(FATAL_ERROR "Currently unsupported CPU architecture") endif() +add_subdirectory(third_party/teakra EXCLUDE_FROM_ALL) set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp - src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp + src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp src/miniaudio.cpp ) set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp) set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp @@ -191,6 +193,9 @@ set(FS_SOURCE_FILES src/core/fs/archive_self_ncch.cpp src/core/fs/archive_save_d set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selector.cpp src/core/applets/software_keyboard.cpp src/core/applets/applet_manager.cpp 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 +) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files @@ -247,6 +252,8 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.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 ) cmrc_add_resource_library( @@ -299,6 +306,7 @@ source_group("Source Files\\Core\\Loader" FILES ${LOADER_SOURCE_FILES}) source_group("Source Files\\Core\\Services" FILES ${SERVICE_SOURCE_FILES}) source_group("Source Files\\Core\\Applets" FILES ${APPLET_SOURCE_FILES}) source_group("Source Files\\Core\\PICA" FILES ${PICA_SOURCE_FILES}) +source_group("Source Files\\Core\\Audio" FILES ${AUDIO_SOURCE_FILES}) source_group("Source Files\\Core\\Software Renderer" FILES ${RENDERER_SW_SOURCE_FILES}) source_group("Source Files\\Third Party" FILES ${THIRD_PARTY_SOURCE_FILES}) @@ -397,7 +405,7 @@ endif() source_group("Header Files\\Core" FILES ${HEADER_FILES}) set(ALL_SOURCES ${SOURCE_FILES} ${FRONTEND_SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} ${APPLET_SOURCE_FILES} ${RENDERER_SW_SOURCE_FILES} ${PICA_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} - ${HEADER_FILES} ${FRONTEND_HEADER_FILES}) + ${AUDIO_SOURCE_FILES} ${HEADER_FILES} ${FRONTEND_HEADER_FILES}) if(ENABLE_OPENGL) # Add the OpenGL source files to ALL_SOURCES @@ -429,7 +437,7 @@ if(ENABLE_LTO OR ENABLE_USER_BUILD) set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) endif() -target_link_libraries(Alber PRIVATE dynarmic cryptopp glad resources_console_fonts) +target_link_libraries(Alber PRIVATE dynarmic cryptopp glad resources_console_fonts teakra) if(NOT ANDROID) target_link_libraries(Alber PRIVATE SDL2-static) diff --git a/include/audio/dsp_core.hpp b/include/audio/dsp_core.hpp new file mode 100644 index 000000000..1a556f289 --- /dev/null +++ b/include/audio/dsp_core.hpp @@ -0,0 +1,66 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "helpers.hpp" +#include "logger.hpp" +#include "scheduler.hpp" +#include "ring_buffer.hpp" + +// The DSP core must have access to the DSP service to be able to trigger interrupts properly +class DSPService; +class Memory; + +namespace Audio { + // There are 160 stereo samples in 1 audio frame, so 320 samples total + static constexpr u64 samplesInFrame = 160; + // 1 frame = 4096 DSP cycles = 8192 ARM11 cycles + static constexpr u64 cyclesPerFrame = samplesInFrame * 8192; + // For LLE DSP cores, we run the DSP for N cycles at a time, every N*2 arm11 cycles since the ARM11 runs twice as fast + static constexpr u64 lleSlice = 16384; + + class DSPCore { + using Samples = Common::RingBuffer; + + protected: + Memory& mem; + Scheduler& scheduler; + DSPService& dspService; + + Samples sampleBuffer; + bool audioEnabled = false; + + MAKE_LOG_FUNCTION(log, dspLogger) + + public: + enum class Type { Null, Teakra }; + DSPCore(Memory& mem, Scheduler& scheduler, DSPService& dspService) + : mem(mem), scheduler(scheduler), dspService(dspService) {} + virtual ~DSPCore() {} + + virtual void reset() = 0; + virtual void runAudioFrame() = 0; + virtual u8* getDspMemory() = 0; + + virtual u16 recvData(u32 regId) = 0; + virtual bool recvDataIsReady(u32 regId) = 0; + virtual void setSemaphore(u16 value) = 0; + virtual void writeProcessPipe(u32 channel, u32 size, u32 buffer) = 0; + virtual std::vector readPipe(u32 channel, u32 peer, u32 size, u32 buffer) = 0; + virtual void loadComponent(std::vector& data, u32 programMask, u32 dataMask) = 0; + virtual void unloadComponent() = 0; + virtual void setSemaphoreMask(u16 value) = 0; + + static Audio::DSPCore::Type typeFromString(std::string inString); + static const char* typeToString(Audio::DSPCore::Type type); + + Samples& getSamples() { return sampleBuffer; } + virtual void setAudioEnabled(bool enable) { audioEnabled = enable; } + }; + + std::unique_ptr makeDSPCore(DSPCore::Type type, Memory& mem, Scheduler& scheduler, DSPService& dspService); +} // namespace Audio \ No newline at end of file diff --git a/include/audio/miniaudio_device.hpp b/include/audio/miniaudio_device.hpp new file mode 100644 index 000000000..f4d126d81 --- /dev/null +++ b/include/audio/miniaudio_device.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include + +#include "miniaudio.h" +#include "ring_buffer.hpp" + +class MiniAudioDevice { + using Samples = Common::RingBuffer; + static constexpr ma_uint32 sampleRate = 32768; // 3DS sample rate + static constexpr ma_uint32 channelCount = 2; // Audio output is stereo + + ma_context context; + ma_device_config deviceConfig; + ma_device device; + ma_resampler resampler; + Samples* samples = nullptr; + + bool initialized = false; + bool running = false; + + std::vector audioDevices; + public: + MiniAudioDevice(); + // If safe is on, we create a null audio device + void init(Samples& samples, bool safe = false); + + void start(); + void stop(); +}; \ No newline at end of file diff --git a/include/audio/null_core.hpp b/include/audio/null_core.hpp new file mode 100644 index 000000000..7d6f1c9e6 --- /dev/null +++ b/include/audio/null_core.hpp @@ -0,0 +1,46 @@ +#pragma once +#include + +#include "audio/dsp_core.hpp" +#include "memory.hpp" + +namespace Audio { + class NullDSP : public DSPCore { + 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 dspRam; + + void resetAudioPipe(); + bool loaded = false; // Have we loaded a component? + + public: + NullDSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) : DSPCore(mem, scheduler, dspService) {} + ~NullDSP() override {} + + void reset() override; + void runAudioFrame() override; + + u8* getDspMemory() override { return dspRam.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; + + // NOPs for null DSP core + 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/audio/teakra_core.hpp b/include/audio/teakra_core.hpp new file mode 100644 index 000000000..6a0112318 --- /dev/null +++ b/include/audio/teakra_core.hpp @@ -0,0 +1,104 @@ +#pragma once +#include + +#include "audio/dsp_core.hpp" +#include "memory.hpp" +#include "swap.hpp" +#include "teakra/teakra.h" + +namespace Audio { + class TeakraDSP : public DSPCore { + Teakra::Teakra teakra; + u32 pipeBaseAddr; + bool running; // Is the DSP running? + bool loaded; // Have we finished loading a binary with LoadComponent? + bool signalledData; + bool signalledSemaphore; + + uint audioFrameIndex = 0; // Index in our audio frame + std::array audioFrame; + + // Get a pointer to a data memory address + u8* getDataPointer(u32 address) { return getDspMemory() + Memory::DSP_DATA_MEMORY_OFFSET + address; } + + enum class PipeDirection { + DSPtoCPU = 0, + CPUtoDSP = 1, + }; + + // A lot of Teakra integration code, especially pipe stuff is based on Citra's integration here: + // https://github.com/citra-emu/citra/blob/master/src/audio_core/lle/lle.cpp + struct PipeStatus { + // All addresses and sizes here refer to byte values, NOT 16-bit values. + u16_le address; + u16_le byteSize; + u16_le readPointer; + u16_le writePointer; + u8 slot; + u8 flags; + + static constexpr u16 wrapBit = 0x8000; + static constexpr u16 pointerMask = 0x7FFF; + + bool isFull() const { return (readPointer ^ writePointer) == wrapBit; } + bool isEmpty() const { return (readPointer ^ writePointer) == 0; } + + // isWrapped: Are read and write pointers in different memory passes. + // true: xxxx]----[xxxx (data is wrapping around the end of memory) + // false: ----[xxxx]---- + bool isWrapped() const { return (readPointer ^ writePointer) >= wrapBit; } + }; + static_assert(sizeof(PipeStatus) == 10, "Teakra: Pipe Status size is wrong"); + static constexpr u8 pipeToSlotIndex(u8 pipe, PipeDirection direction) { return (pipe * 2) + u8(direction); } + + PipeStatus getPipeStatus(u8 pipe, PipeDirection direction) { + PipeStatus ret; + const u8 index = pipeToSlotIndex(pipe, direction); + + std::memcpy(&ret, getDataPointer(pipeBaseAddr * 2 + index * sizeof(PipeStatus)), sizeof(PipeStatus)); + return ret; + } + + void updatePipeStatus(const PipeStatus& status) { + u8 slot = status.slot; + u8* statusAddress = getDataPointer(pipeBaseAddr * 2 + slot * sizeof(PipeStatus)); + + if (slot % 2 == 0) { + std::memcpy(statusAddress + 4, &status.readPointer, sizeof(u16)); + } else { + std::memcpy(statusAddress + 6, &status.writePointer, sizeof(u16)); + } + } + // Run 1 slice of DSP instructions + void runSlice() { + if (running) { + teakra.Run(Audio::lleSlice); + } + } + + public: + TeakraDSP(Memory& mem, Scheduler& scheduler, DSPService& dspService); + ~TeakraDSP() override {} + + void reset() override; + + // Run 1 slice of DSP instructions and schedule the next audio frame + void runAudioFrame() override { + runSlice(); + scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::lleSlice * 2); + } + + void setAudioEnabled(bool enable) override; + u8* getDspMemory() override { return teakra.GetDspMemory().data(); } + + u16 recvData(u32 regId) override { return teakra.RecvData(regId); } + bool recvDataIsReady(u32 regId) override { return teakra.RecvDataIsReady(regId); } + void setSemaphore(u16 value) override { teakra.SetSemaphore(value); } + void setSemaphoreMask(u16 value) override { teakra.MaskSemaphore(value); } + + 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; + }; +} // namespace Audio diff --git a/include/config.hpp b/include/config.hpp index 155f5961d..8c0d2e123 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include "audio/dsp_core.hpp" #include "renderer.hpp" // Remember to initialize every field here to its default value otherwise bad things will happen @@ -15,11 +16,15 @@ struct EmulatorConfig { bool shaderJitEnabled = shaderJitDefault; bool discordRpcEnabled = false; RendererType rendererType = RendererType::OpenGL; + Audio::DSPCore::Type dspType = Audio::DSPCore::Type::Null; bool sdCardInserted = true; bool sdWriteProtected = false; bool usePortableBuild = false; + bool audioEnabled = false; + bool vsyncEnabled = true; + bool chargerPlugged = true; // Default to 3% battery to make users suffer int batteryPercentage = 3; diff --git a/include/emulator.hpp b/include/emulator.hpp index d3377f6c9..47fbc839f 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -2,10 +2,13 @@ #include #include +#include #include #include #include "PICA/gpu.hpp" +#include "audio/dsp_core.hpp" +#include "audio/miniaudio_device.hpp" #include "cheats.hpp" #include "config.hpp" #include "cpu.hpp" @@ -41,9 +44,12 @@ class Emulator { GPU gpu; Memory memory; Kernel kernel; + std::unique_ptr dsp; + Scheduler scheduler; + Crypto::AESEngine aesEngine; + MiniAudioDevice audioDevice; Cheats cheats; - Scheduler scheduler; // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state @@ -71,6 +77,7 @@ class Emulator { #ifdef PANDA3DS_ENABLE_DISCORD_RPC Discord::RPC discordRpc; #endif + void setAudioEnabled(bool enable); void updateDiscord(); // Keep the handle for the ROM here to reload when necessary and to prevent deleting it @@ -128,6 +135,7 @@ class Emulator { ServiceManager& getServiceManager() { return kernel.getServiceManager(); } LuaManager& getLua() { return lua; } Scheduler& getScheduler() { return scheduler; } + Memory& getMemory() { return memory; } RendererType getRendererType() const { return config.rendererType; } Renderer* getRenderer() { return gpu.getRenderer(); } diff --git a/include/kernel/kernel.hpp b/include/kernel/kernel.hpp index e78a588a9..fc7fe3f32 100644 --- a/include/kernel/kernel.hpp +++ b/include/kernel/kernel.hpp @@ -66,15 +66,20 @@ class Kernel { Handle makeMemoryBlock(u32 addr, u32 size, u32 myPermission, u32 otherPermission); public: - Handle makeEvent(ResetType resetType); // Needs to be public to be accessible to the APT/HID services - Handle makeMutex(bool locked = false); // Needs to be public to be accessible to the APT/DSP services - Handle makeSemaphore(u32 initialCount, u32 maximumCount); // Needs to be public to be accessible to the service manager port + // Needs to be public to be accessible to the APT/HID services + Handle makeEvent(ResetType resetType, Event::CallbackType callback = Event::CallbackType::None); + // Needs to be public to be accessible to the APT/DSP services + Handle makeMutex(bool locked = false); + // Needs to be public to be accessible to the service manager port + Handle makeSemaphore(u32 initialCount, u32 maximumCount); Handle makeTimer(ResetType resetType); void pollTimers(); // Signals an event, returns true on success or false if the event does not exist bool signalEvent(Handle e); - + // Run the callback for "special" events that have callbacks + void runEventCallback(Event::CallbackType callback); + void clearEvent(Handle e) { KernelObject* object = getObject(e, KernelObjectType::Event); if (object != nullptr) { @@ -240,6 +245,5 @@ class Kernel { ServiceManager& getServiceManager() { return serviceManager; } void sendGPUInterrupt(GPUInterrupt type) { serviceManager.sendGPUInterrupt(type); } - void signalDSPEvents() { serviceManager.signalDSPEvents(); } void clearInstructionCache(); }; \ No newline at end of file diff --git a/include/kernel/kernel_types.hpp b/include/kernel/kernel_types.hpp index 01af4bd90..a68ef8d54 100644 --- a/include/kernel/kernel_types.hpp +++ b/include/kernel/kernel_types.hpp @@ -62,11 +62,19 @@ struct Process { }; struct Event { + // Some events (for now, only the DSP semaphore events) need to execute a callback when signalled + // This enum stores what kind of callback they should execute + enum class CallbackType : u32 { + None, DSPSemaphore, + }; + u64 waitlist; // A bitfield where each bit symbolizes if the thread with thread with the corresponding index is waiting on the event ResetType resetType = ResetType::OneShot; + CallbackType callback = CallbackType::None; bool fired = false; Event(ResetType resetType) : resetType(resetType), waitlist(0) {} + Event(ResetType resetType, CallbackType cb) : resetType(resetType), waitlist(0), callback(cb) {} }; struct Port { diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp index c5ef24656..42ce1590f 100644 --- a/include/loader/ncch.hpp +++ b/include/loader/ncch.hpp @@ -36,6 +36,7 @@ struct NCCH { }; u64 partitionIndex = 0; + u64 programID = 0; u64 fileOffset = 0; bool isNew3DS = false; diff --git a/include/logger.hpp b/include/logger.hpp index e021a685d..4fc521b6c 100644 --- a/include/logger.hpp +++ b/include/logger.hpp @@ -36,6 +36,7 @@ namespace Log { static Logger gpuLogger; static Logger rendererLogger; static Logger shaderJITLogger; + static Logger dspLogger; // Service loggers static Logger acLogger; diff --git a/include/lua_manager.hpp b/include/lua_manager.hpp index 50b8dd612..46fd553a3 100644 --- a/include/lua_manager.hpp +++ b/include/lua_manager.hpp @@ -2,7 +2,6 @@ #include #include "helpers.hpp" -#include "memory.hpp" // The kinds of events that can cause a Lua call. // Frame: Call program on frame end @@ -11,6 +10,8 @@ enum class LuaEvent { Frame, }; +class Emulator; + #ifdef PANDA3DS_ENABLE_LUA extern "C" { #include @@ -30,9 +31,9 @@ class LuaManager { public: // For Lua we must have some global pointers to our emulator objects to use them in script code via thunks. See the thunks in lua.cpp as an // example - static Memory* g_memory; + static Emulator* g_emulator; - LuaManager(Memory& mem) { g_memory = &mem; } + LuaManager(Emulator& emulator) { g_emulator = &emulator; } void close(); void initialize(); @@ -51,7 +52,7 @@ class LuaManager { #else // Lua not enabled, Lua manager does nothing class LuaManager { public: - LuaManager(Memory& mem) {} + LuaManager(Emulator& emulator) {} void close() {} void initialize() {} diff --git a/include/memory.hpp b/include/memory.hpp index 640ae5f00..1b6e622ca 100644 --- a/include/memory.hpp +++ b/include/memory.hpp @@ -100,8 +100,8 @@ namespace KernelMemoryTypes { class Memory { u8* fcram; - u8* dspRam; - u8* vram; // Provided to the memory class by the GPU class + u8* dspRam; // Provided to us by Audio + u8* vram; // Provided to the memory class by the GPU class u64& cpuTicks; // Reference to the CPU tick counter using SharedMemoryBlock = KernelMemoryTypes::SharedMemoryBlock; @@ -275,12 +275,16 @@ class Memory { // File handle for reading the loaded ncch IOFile CXIFile; + std::optional getProgramID(); + u8* getDSPMem() { return dspRam; } u8* getDSPDataMem() { return &dspRam[DSP_DATA_MEMORY_OFFSET]; } u8* getDSPCodeMem() { return &dspRam[DSP_CODE_MEMORY_OFFSET]; } u32 getUsedUserMem() { return usedUserMemory; } void setVRAM(u8* pointer) { vram = pointer; } + void setDSPMem(u8* pointer) { dspRam = pointer; } + bool allocateMainThreadStack(u32 size); Regions getConsoleRegion(); void copySharedFont(u8* ptr); diff --git a/include/ring_buffer.hpp b/include/ring_buffer.hpp new file mode 100644 index 000000000..35d7d9350 --- /dev/null +++ b/include/ring_buffer.hpp @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Common { + /// SPSC ring buffer + /// @tparam T Element type + /// @tparam capacity Number of slots in ring buffer + template + class RingBuffer { + /// A "slot" is made of a single `T`. + static constexpr std::size_t slot_size = sizeof(T); + // T must be safely memcpy-able and have a trivial default constructor. + static_assert(std::is_trivial_v); + // Ensure capacity is sensible. + static_assert(capacity < std::numeric_limits::max() / 2); + static_assert((capacity & (capacity - 1)) == 0, "capacity must be a power of two"); + // Ensure lock-free. + static_assert(std::atomic_size_t::is_always_lock_free); + + public: + /// Pushes slots into the ring buffer + /// @param new_slots Pointer to the slots to push + /// @param slot_count Number of slots to push + /// @returns The number of slots actually pushed + std::size_t push(const void* new_slots, std::size_t slot_count) { + const std::size_t write_index = m_write_index.load(); + const std::size_t slots_free = capacity + m_read_index.load() - write_index; + const std::size_t push_count = std::min(slot_count, slots_free); + + const std::size_t pos = write_index % capacity; + const std::size_t first_copy = std::min(capacity - pos, push_count); + const std::size_t second_copy = push_count - first_copy; + + const char* in = static_cast(new_slots); + std::memcpy(m_data.data() + pos, in, first_copy * slot_size); + in += first_copy * slot_size; + std::memcpy(m_data.data(), in, second_copy * slot_size); + + m_write_index.store(write_index + push_count); + + return push_count; + } + + std::size_t push(std::span input) { return push(input.data(), input.size()); } + + /// Pops slots from the ring buffer + /// @param output Where to store the popped slots + /// @param max_slots Maximum number of slots to pop + /// @returns The number of slots actually popped + std::size_t pop(void* output, std::size_t max_slots = ~std::size_t(0)) { + const std::size_t read_index = m_read_index.load(); + const std::size_t slots_filled = m_write_index.load() - read_index; + const std::size_t pop_count = std::min(slots_filled, max_slots); + + const std::size_t pos = read_index % capacity; + const std::size_t first_copy = std::min(capacity - pos, pop_count); + const std::size_t second_copy = pop_count - first_copy; + + char* out = static_cast(output); + std::memcpy(out, m_data.data() + pos, first_copy * slot_size); + out += first_copy * slot_size; + std::memcpy(out, m_data.data(), second_copy * slot_size); + + m_read_index.store(read_index + pop_count); + + return pop_count; + } + + std::vector pop(std::size_t max_slots = ~std::size_t(0)) { + std::vector out(std::min(max_slots, capacity)); + const std::size_t count = Pop(out.data(), out.size()); + out.resize(count); + return out; + } + + /// @returns Number of slots used + [[nodiscard]] std::size_t size() const { return m_write_index.load() - m_read_index.load(); } + + /// @returns Maximum size of ring buffer + [[nodiscard]] constexpr std::size_t Capacity() const { return capacity; } + + private: + // It is important to align the below variables for performance reasons: + // Having them on the same cache-line would result in false-sharing between them. + // TODO: Remove this ifdef whenever clang and GCC support + // std::hardware_destructive_interference_size. +#if defined(__cpp_lib_hardware_interference_size) && !defined(__ANDROID__) + alignas(std::hardware_destructive_interference_size) std::atomic_size_t m_read_index{0}; + alignas(std::hardware_destructive_interference_size) std::atomic_size_t m_write_index{0}; +#else + alignas(128) std::atomic_size_t m_read_index{0}; + alignas(128) std::atomic_size_t m_write_index{0}; +#endif + + std::array m_data; + }; + +} // namespace Common diff --git a/include/scheduler.hpp b/include/scheduler.hpp index 5645f47d5..97c50afc9 100644 --- a/include/scheduler.hpp +++ b/include/scheduler.hpp @@ -10,7 +10,8 @@ struct Scheduler { enum class EventType { VBlank = 0, // End of frame event UpdateTimers = 1, // Update kernel timer objects - Panic = 2, // Dummy event that is always pending and should never be triggered (Timestamp = UINT64_MAX) + RunDSP = 2, // Make the emulated DSP run for one audio frame + Panic = 3, // Dummy event that is always pending and should never be triggered (Timestamp = UINT64_MAX) TotalNumberOfEvents // How many event types do we have in total? }; static constexpr usize totalNumberOfEvents = static_cast(EventType::TotalNumberOfEvents); diff --git a/include/services/dsp.hpp b/include/services/dsp.hpp index ab9fb106e..bc27377d3 100644 --- a/include/services/dsp.hpp +++ b/include/services/dsp.hpp @@ -1,17 +1,12 @@ #pragma once #include #include +#include "audio/dsp_core.hpp" #include "helpers.hpp" #include "logger.hpp" #include "memory.hpp" #include "result/result.hpp" -namespace DSPPipeType { - enum : u32 { - Debug = 0, DMA = 1, Audio = 2, Binary = 3 - }; -} - // Circular dependencies! class Kernel; @@ -19,15 +14,11 @@ class DSPService { Handle handle = KernelHandles::DSP; Memory& mem; Kernel& kernel; + Audio::DSPCore* dsp = nullptr; MAKE_LOG_FUNCTION(log, dspServiceLogger) - enum class DSPState : u32 { - Off, On, Slep - }; - // Number of DSP pipes static constexpr size_t pipeCount = 8; - DSPState dspState; // DSP service event handles using DSPEvent = std::optional; @@ -36,10 +27,7 @@ class DSPService { DSPEvent interrupt0; DSPEvent interrupt1; std::array pipeEvents; - std::array, pipeCount> pipeData; // The data of each pipe - - void resetAudioPipe(); - std::vector readPipe(u32 pipe, u32 size); + u16 semaphoreMask = 0; DSPEvent& getEventRef(u32 type, u32 pipe); static constexpr size_t maxEventCount = 6; @@ -67,6 +55,10 @@ class DSPService { DSPService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); + void setDSPCore(Audio::DSPCore* pointer) { dsp = pointer; } + + // Special callback that's ran when the semaphore event is signalled + void onSemaphoreEventSignal() { dsp->setSemaphore(semaphoreMask); } enum class SoundOutputMode : u8 { Mono = 0, @@ -74,5 +66,8 @@ class DSPService { Surround = 2 }; - void signalEvents(); + void triggerPipeEvent(int index); + void triggerSemaphoreEvent(); + void triggerInterrupt0(); + void triggerInterrupt1(); }; \ No newline at end of file diff --git a/include/services/service_manager.hpp b/include/services/service_manager.hpp index 93700498f..8d1cf3817 100644 --- a/include/services/service_manager.hpp +++ b/include/services/service_manager.hpp @@ -105,9 +105,8 @@ class ServiceManager { void setHIDSharedMem(u8* ptr) { hid.setSharedMem(ptr); } void setCSNDSharedMem(u8* ptr) { csnd.setSharedMemory(ptr); } - void signalDSPEvents() { dsp.signalEvents(); } - // Input function wrappers HIDService& getHID() { return hid; } NFCService& getNFC() { return nfc; } + DSPService& getDSP() { return dsp; } }; diff --git a/src/config.cpp b/src/config.cpp index cd4e1f797..f19ff06dd 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -59,6 +59,18 @@ void EmulatorConfig::load() { } shaderJitEnabled = toml::find_or(gpu, "EnableShaderJIT", shaderJitDefault); + vsyncEnabled = toml::find_or(gpu, "EnableVSync", true); + } + } + + if (data.contains("Audio")) { + auto audioResult = toml::expect(data.at("Audio")); + if (audioResult.is_ok()) { + auto audio = audioResult.unwrap(); + + auto dspCoreName = toml::find_or(audio, "DSPEmulation", "Null"); + dspType = Audio::DSPCore::typeFromString(dspCoreName); + audioEnabled = toml::find_or(audio, "EnableAudio", false); } } @@ -109,6 +121,9 @@ void EmulatorConfig::save() { data["General"]["UsePortableBuild"] = usePortableBuild; data["GPU"]["EnableShaderJIT"] = shaderJitEnabled; data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType)); + data["GPU"]["EnableVSync"] = vsyncEnabled; + data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType)); + data["Audio"]["EnableAudio"] = audioEnabled; data["Battery"]["ChargerPlugged"] = chargerPlugged; data["Battery"]["BatteryPercentage"] = batteryPercentage; diff --git a/src/core/audio/dsp_core.cpp b/src/core/audio/dsp_core.cpp new file mode 100644 index 000000000..e4162e939 --- /dev/null +++ b/src/core/audio/dsp_core.cpp @@ -0,0 +1,52 @@ +#include "audio/dsp_core.hpp" + +#include "audio/null_core.hpp" +#include "audio/teakra_core.hpp" + +#include +#include +#include + +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; + + default: + Helpers::warn("Invalid DSP core selected!"); + core = std::make_unique(mem, scheduler, dspService); + break; + } + + mem.setDSPMem(core->getDspMemory()); + return core; +} + +Audio::DSPCore::Type Audio::DSPCore::typeFromString(std::string inString) { + // Transform to lower-case to make the setting case-insensitive + 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}, + }; + + if (auto search = map.find(inString); search != map.end()) { + return search->second; + } + + printf("Invalid DSP type. Defaulting to null\n"); + return Audio::DSPCore::Type::Null; +} + +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"; + default: return "invalid"; + } +} diff --git a/src/core/audio/miniaudio_device.cpp b/src/core/audio/miniaudio_device.cpp new file mode 100644 index 000000000..fa36cb842 --- /dev/null +++ b/src/core/audio/miniaudio_device.cpp @@ -0,0 +1,143 @@ +#include "audio/miniaudio_device.hpp" + +#include "helpers.hpp" + +MiniAudioDevice::MiniAudioDevice() : initialized(false), running(false), samples(nullptr) {} + +void MiniAudioDevice::init(Samples& samples, bool safe) { + this->samples = &samples; + running = false; + + // Probe for device and available backends and initialize audio + ma_backend backends[ma_backend_null + 1]; + uint count = 0; + + if (safe) { + backends[0] = ma_backend_null; + count = 1; + } else { + bool found = false; + for (uint i = 0; i <= ma_backend_null; i++) { + ma_backend backend = ma_backend(i); + if (!ma_is_backend_enabled(backend)) { + continue; + } + + backends[count++] = backend; + + // TODO: Make backend selectable here + found = true; + //count = 1; + //backends[0] = backend; + } + + if (!found) { + initialized = false; + + Helpers::warn("No valid audio backend found\n"); + return; + } + } + + if (ma_context_init(backends, count, nullptr, &context) != MA_SUCCESS) { + initialized = false; + + Helpers::warn("Unable to initialize audio context"); + return; + } + audioDevices.clear(); + + struct UserContext { + MiniAudioDevice* miniAudio; + ma_device_config& config; + bool found = false; + }; + UserContext userContext = {.miniAudio = this, .config = deviceConfig}; + + ma_context_enumerate_devices( + &context, + [](ma_context* pContext, ma_device_type deviceType, const ma_device_info* pInfo, void* pUserData) -> ma_bool32 { + if (deviceType != ma_device_type_playback) { + return true; + } + + UserContext* userContext = reinterpret_cast(pUserData); + userContext->miniAudio->audioDevices.push_back(pInfo->name); + + // TODO: Check if this is the device we want here + userContext->config.playback.pDeviceID = &pInfo->id; + userContext->found = true; + return true; + }, + &userContext + ); + + if (!userContext.found) { + Helpers::warn("MiniAudio: Device not found"); + } + + deviceConfig = ma_device_config_init(ma_device_type_playback); + // The 3DS outputs s16 stereo audio @ 32768 Hz + deviceConfig.playback.format = ma_format_s16; + deviceConfig.playback.channels = channelCount; + deviceConfig.sampleRate = sampleRate; + //deviceConfig.periodSizeInFrames = 64; + //deviceConfig.periods = 16; + deviceConfig.pUserData = this; + deviceConfig.aaudio.usage = ma_aaudio_usage_game; + deviceConfig.wasapi.noAutoConvertSRC = true; + + deviceConfig.dataCallback = [](ma_device* device, void* out, const void* input, ma_uint32 frameCount) { + auto self = reinterpret_cast(device->pUserData); + s16* output = reinterpret_cast(out); + + // Wait until there's enough samples to pop + while (self->samples->size() < frameCount * channelCount) { + // If audio output is disabled from the emulator thread, make sure that this callback will return and not hang + if (!self->running) { + return; + } + } + + self->samples->pop(output, frameCount * channelCount); + }; + + if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) { + Helpers::warn("Unable to initialize audio device"); + initialized = false; + return; + } + + initialized = true; +} + +void MiniAudioDevice::start() { + if (!initialized) { + Helpers::warn("MiniAudio device not initialized, won't start"); + return; + } + + // Ignore the call to start if the device is already running + if (!running) { + if (ma_device_start(&device) == MA_SUCCESS) { + running = true; + } else { + Helpers::warn("Failed to start audio device"); + } + } +} + +void MiniAudioDevice::stop() { + if (!initialized) { + Helpers::warn("MiniAudio device not initialized, can't start"); + return; + } + + if (running) { + running = false; + + if (ma_device_stop(&device) != MA_SUCCESS) { + Helpers::warn("Failed to stop audio device"); + } + } +} diff --git a/src/core/audio/null_core.cpp b/src/core/audio/null_core.cpp new file mode 100644 index 000000000..ec073ae74 --- /dev/null +++ b/src/core/audio/null_core.cpp @@ -0,0 +1,166 @@ +#include "audio/null_core.hpp" + +#include "services/dsp.hpp" + +namespace Audio { + namespace DSPPipeType { + enum : u32 { + Debug = 0, + DMA = 1, + Audio = 2, + Binary = 3, + }; + } + + void NullDSP::resetAudioPipe() { + // Hardcoded responses for now + // These are DSP DRAM offsets for various variables + // https://www.3dbrew.org/wiki/DSP_Memory_Region + static constexpr std::array responses = { + 0x000F, // Number of responses + 0xBFFF, // Frame counter + 0x9E92, // Source configs + 0x8680, // Source statuses + 0xA792, // ADPCM coefficients + 0x9430, // DSP configs + 0x8400, // DSP status + 0x8540, // Final samples + 0x9492, // Intermediate mix samples + 0x8710, // Compressor + 0x8410, // Debug + 0xA912, // ?? + 0xAA12, // ?? + 0xAAD2, // ?? + 0xAC52, // Surround sound biquad filter 1 + 0xAC5C // Surround sound biquad filter 2 + }; + + 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 NullDSP::reset() { + loaded = false; + for (auto& e : pipeData) { + e.clear(); + } + + // Note: Reset audio pipe AFTER resetting all pipes, otherwise the new data will be yeeted + resetAudioPipe(); + } + + void NullDSP::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 NullDSP::unloadComponent() { + if (!loaded) { + Helpers::warn("Audio: unloadComponent called without a running program"); + } + + loaded = false; + scheduler.removeEvent(Scheduler::EventType::RunDSP); + } + + void NullDSP::runAudioFrame() { + // Signal audio pipe when an audio frame is done + if (dspState == DSPState::On) [[likely]] { + dspService.triggerPipeEvent(DSPPipeType::Audio); + } + + scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); + } + + u16 NullDSP::recvData(u32 regId) { + if (regId != 0) { + Helpers::panic("Audio: invalid register in null frontend"); + } + + return dspState == DSPState::On; + } + + void NullDSP::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::NullDSP: Wrote to unimplemented pipe %d\n", channel); break; + } + } + + std::vector NullDSP::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; + } +} // namespace Audio diff --git a/src/core/audio/teakra_core.cpp b/src/core/audio/teakra_core.cpp new file mode 100644 index 000000000..da2e5a5ac --- /dev/null +++ b/src/core/audio/teakra_core.cpp @@ -0,0 +1,345 @@ +#include "audio/teakra_core.hpp" + +#include +#include +#include +#include + +#include "services/dsp.hpp" + +using namespace Audio; + +struct Dsp1 { + // All sizes are in bytes unless otherwise specified + u8 signature[0x100]; + u8 magic[4]; + u32 size; + u8 codeMemLayout; + u8 dataMemLayout; + u8 pad[3]; + u8 specialType; + u8 segmentCount; + u8 flags; + u32 specialStart; + u32 specialSize; + u64 zeroBits; + + struct Segment { + u32 offs; // Offset of the segment data + u32 dspAddr; // Start of the segment in 16-bit units + u32 size; + u8 pad[3]; + u8 type; + u8 hash[0x20]; + }; + + Segment segments[10]; +}; + +TeakraDSP::TeakraDSP(Memory& mem, Scheduler& scheduler, DSPService& dspService) + : DSPCore(mem, scheduler, dspService), pipeBaseAddr(0), running(false) { + // Set up callbacks for Teakra + Teakra::AHBMCallback ahbm; + + // The AHBM read handlers read from paddrs rather than vaddrs which mem.read8 and the like use + // TODO: When we implement more efficient paddr accesses with a page table or similar, these handlers + // Should be made to properly use it, since this method is hacky and will segfault if given an invalid addr + ahbm.read8 = [&](u32 addr) -> u8 { return mem.getFCRAM()[addr - PhysicalAddrs::FCRAM]; }; + ahbm.read16 = [&](u32 addr) -> u16 { return *(u16*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM]; }; + ahbm.read32 = [&](u32 addr) -> u32 { return *(u32*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM]; }; + + ahbm.write8 = [&](u32 addr, u8 value) { mem.getFCRAM()[addr - PhysicalAddrs::FCRAM] = value; }; + ahbm.write16 = [&](u32 addr, u16 value) { *(u16*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM] = value; }; + ahbm.write32 = [&](u32 addr, u32 value) { *(u32*)&mem.getFCRAM()[addr - PhysicalAddrs::FCRAM] = value; }; + + teakra.SetAHBMCallback(ahbm); + teakra.SetAudioCallback([](std::array sample) { /* Do nothing */ }); + + // Set up event handlers. These handlers forward a hardware interrupt to the DSP service, which is responsible + // For triggering the appropriate DSP kernel events + // Note: It's important not to fire any events if "loaded" is false, ie if we haven't fully loaded a DSP component yet + teakra.SetRecvDataHandler(0, [&]() { + if (loaded) { + dspService.triggerInterrupt0(); + } + }); + + teakra.SetRecvDataHandler(1, [&]() { + if (loaded) { + dspService.triggerInterrupt1(); + } + }); + + auto processPipeEvent = [&](bool dataEvent) { + if (!loaded) { + return; + } + + if (dataEvent) { + signalledData = true; + } else { + if ((teakra.GetSemaphore() & 0x8000) == 0) { + return; + } + + signalledSemaphore = true; + } + + if (signalledSemaphore && signalledData) { + signalledSemaphore = signalledData = false; + + u16 slot = teakra.RecvData(2); + u16 side = slot & 1; + u16 pipe = slot / 2; + + if (side != static_cast(PipeDirection::DSPtoCPU)) { + return; + } + + if (pipe == 0) { + Helpers::warn("Pipe event for debug pipe: Should be ignored and the data should be flushed"); + } else { + dspService.triggerPipeEvent(pipe); + } + } + }; + + teakra.SetRecvDataHandler(2, [processPipeEvent]() { processPipeEvent(true); }); + teakra.SetSemaphoreHandler([processPipeEvent]() { processPipeEvent(false); }); +} + +void TeakraDSP::reset() { + teakra.Reset(); + running = false; + loaded = false; + signalledData = signalledSemaphore = false; + + audioFrameIndex = 0; +} + +void TeakraDSP::setAudioEnabled(bool enable) { + if (audioEnabled != enable) { + audioEnabled = enable; + + // Set the appropriate audio callback for Teakra + if (audioEnabled) { + teakra.SetAudioCallback([this](std::array sample) { + audioFrame[audioFrameIndex++] = sample[0]; + audioFrame[audioFrameIndex++] = sample[1]; + + // Push our samples at the end of an audio frame + if (audioFrameIndex >= audioFrame.size()) { + audioFrameIndex -= audioFrame.size(); + + // Wait until we've actually got room to do so + while (sampleBuffer.size() + 2 > sampleBuffer.Capacity()) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } + + sampleBuffer.push(audioFrame.data(), audioFrame.size()); + } + }); + } else { + teakra.SetAudioCallback([](std::array sample) { /* Do nothing */ }); + } + } +} + +// https://github.com/citra-emu/citra/blob/master/src/audio_core/lle/lle.cpp +void TeakraDSP::writeProcessPipe(u32 channel, u32 size, u32 buffer) { + size &= 0xffff; + + PipeStatus status = getPipeStatus(channel, PipeDirection::CPUtoDSP); + bool needUpdate = false; // Do we need to update the pipe status and catch up Teakra? + + std::vector data; + data.reserve(size); + + // Read data to write + for (int i = 0; i < size; i++) { + const u8 byte = mem.read8(buffer + i); + data.push_back(byte); + } + u8* dataPointer = data.data(); + + while (size != 0) { + if (status.isFull()) { + Helpers::warn("Teakra: Writing to full pipe"); + } + + // Calculate begin/end/size for write + const u16 writeEnd = status.isWrapped() ? (status.readPointer & PipeStatus::pointerMask) : status.byteSize; + const u16 writeBegin = status.writePointer & PipeStatus::pointerMask; + const u16 writeSize = std::min(u16(size), writeEnd - writeBegin); + + if (writeEnd <= writeBegin) [[unlikely]] { + Helpers::warn("Teakra: Writing to pipe but end <= start"); + } + + // Write data to pipe, increment write and buffer pointers, decrement size + std::memcpy(getDataPointer(status.address * 2 + writeBegin), dataPointer, writeSize); + dataPointer += writeSize; + status.writePointer += writeSize; + size -= writeSize; + + if ((status.writePointer & PipeStatus::pointerMask) > status.byteSize) [[unlikely]] { + Helpers::warn("Teakra: Writing to pipe but write > size"); + } + + if ((status.writePointer & PipeStatus::pointerMask) == status.byteSize) { + status.writePointer &= PipeStatus::wrapBit; + status.writePointer ^= PipeStatus::wrapBit; + } + needUpdate = true; + } + + if (needUpdate) { + updatePipeStatus(status); + while (!teakra.SendDataIsEmpty(2)) { + runSlice(); + } + + teakra.SendData(2, status.slot); + } +} + +std::vector TeakraDSP::readPipe(u32 channel, u32 peer, u32 size, u32 buffer) { + size &= 0xffff; + + PipeStatus status = getPipeStatus(channel, PipeDirection::DSPtoCPU); + + std::vector pipeData(size); + u8* dataPointer = pipeData.data(); + bool needUpdate = false; // Do we need to update the pipe status and catch up Teakra? + + while (size != 0) { + if (status.isEmpty()) [[unlikely]] { + Helpers::warn("Teakra: Reading from empty pipe"); + return pipeData; + } + + // Read as many bytes as possible + const u16 readEnd = status.isWrapped() ? status.byteSize : (status.writePointer & PipeStatus::pointerMask); + const u16 readBegin = status.readPointer & PipeStatus::pointerMask; + const u16 readSize = std::min(u16(size), readEnd - readBegin); + + // Copy bytes to the output vector, increment the read and vector pointers and decrement the size appropriately + std::memcpy(dataPointer, getDataPointer(status.address * 2 + readBegin), readSize); + dataPointer += readSize; + status.readPointer += readSize; + size -= readSize; + + if ((status.readPointer & PipeStatus::pointerMask) > status.byteSize) [[unlikely]] { + Helpers::warn("Teakra: Reading from pipe but read > size"); + } + + if ((status.readPointer & PipeStatus::pointerMask) == status.byteSize) { + status.readPointer &= PipeStatus::wrapBit; + status.readPointer ^= PipeStatus::wrapBit; + } + + needUpdate = true; + } + + if (needUpdate) { + updatePipeStatus(status); + while (!teakra.SendDataIsEmpty(2)) { + runSlice(); + } + + teakra.SendData(2, status.slot); + } + + return pipeData; +} + +void TeakraDSP::loadComponent(std::vector& data, u32 programMask, u32 dataMask) { + // TODO: maybe move this to the DSP service + if (loaded) { + Helpers::warn("Loading DSP component when already loaded"); + return; + } + + teakra.Reset(); + running = true; + + u8* dspCode = teakra.GetDspMemory().data(); + u8* dspData = dspCode + 0x40000; + + Dsp1 dsp1; + std::memcpy(&dsp1, data.data(), sizeof(dsp1)); + + // TODO: verify DSP1 signature + + // Load DSP segments to DSP RAM + // TODO: verify hashes + for (uint i = 0; i < dsp1.segmentCount; i++) { + auto& segment = dsp1.segments[i]; + u32 addr = segment.dspAddr << 1; + u8* src = data.data() + segment.offs; + u8* dst = nullptr; + + switch (segment.type) { + case 0: + case 1: dst = dspCode + addr; break; + default: dst = dspData + addr; break; + } + + std::memcpy(dst, src, segment.size); + } + + bool syncWithDsp = dsp1.flags & 0x1; + bool loadSpecialSegment = (dsp1.flags >> 1) & 0x1; + + // TODO: how does the special segment work? + if (loadSpecialSegment) { + log("LoadComponent: special segment not supported"); + } + + if (syncWithDsp) { + // Wait for the DSP to reply with 1s in all RECV registers + for (int i = 0; i < 3; i++) { + do { + while (!teakra.RecvDataIsReady(i)) { + runSlice(); + } + } while (teakra.RecvData(i) != 1); + } + } + + // Retrieve the pipe base address + while (!teakra.RecvDataIsReady(2)) { + runSlice(); + } + pipeBaseAddr = teakra.RecvData(2); + + // Schedule next DSP event + scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::lleSlice * 2); + loaded = true; +} + +void TeakraDSP::unloadComponent() { + if (!loaded) { + Helpers::warn("Audio: unloadComponent called without a running program"); + return; + } + loaded = false; + // Stop scheduling DSP events + scheduler.removeEvent(Scheduler::EventType::RunDSP); + + // Wait for SEND2 to be ready, then send the shutdown command to the DSP + while (!teakra.SendDataIsEmpty(2)) { + runSlice(); + } + + teakra.SendData(2, 0x8000); + + // Wait for shutdown to be acknowledged + while (!teakra.RecvDataIsReady(2)) { + runSlice(); + } + + // Read the value and discard it, completing shutdown + teakra.RecvData(2); + running = false; +} diff --git a/src/core/kernel/events.cpp b/src/core/kernel/events.cpp index b2f89fbf5..7c0d30479 100644 --- a/src/core/kernel/events.cpp +++ b/src/core/kernel/events.cpp @@ -12,9 +12,9 @@ const char* Kernel::resetTypeToString(u32 type) { } } -Handle Kernel::makeEvent(ResetType resetType) { +Handle Kernel::makeEvent(ResetType resetType, Event::CallbackType callback) { Handle ret = makeObject(KernelObjectType::Event); - objects[ret].data = new Event(resetType); + objects[ret].data = new Event(resetType, callback); return ret; } @@ -42,8 +42,13 @@ bool Kernel::signalEvent(Handle handle) { event->fired = false; } } - + rescheduleThreads(); + // Run the callback for events that require a special callback + if (event->callback != Event::CallbackType::None) [[unlikely]] { + runEventCallback(event->callback); + } + return true; } @@ -230,4 +235,12 @@ void Kernel::waitSynchronizationN() { } else { Helpers::panic("WaitSynchronizationN with waitAll"); } +} + +void Kernel::runEventCallback(Event::CallbackType callback) { + switch (callback) { + case Event::CallbackType::None: break; + case Event::CallbackType::DSPSemaphore: serviceManager.getDSP().onSemaphoreEventSignal(); break; + default: Helpers::panic("Unimplemented special callback for kernel event!"); break; + } } \ No newline at end of file diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 1f534fa05..3bf73e5d8 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -32,7 +32,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break exheaderSize = *(u32*)&header[0x180]; - const u64 programID = *(u64*)&header[0x118]; + programID = *(u64*)&header[0x118]; // Read NCCH flags secondaryKeySlot = header[0x188 + 3]; diff --git a/src/core/memory.cpp b/src/core/memory.cpp index 83248ef51..09b49eee2 100644 --- a/src/core/memory.cpp +++ b/src/core/memory.cpp @@ -15,7 +15,6 @@ using namespace KernelMemoryTypes; Memory::Memory(u64& cpuTicks, const EmulatorConfig& config) : cpuTicks(cpuTicks), config(config) { fcram = new uint8_t[FCRAM_SIZE](); - dspRam = new uint8_t[DSP_RAM_SIZE](); readTable.resize(totalPageCount, 0); writeTable.resize(totalPageCount, 0); @@ -525,4 +524,14 @@ void Memory::copySharedFont(u8* pointer) { auto fonts = cmrc::ConsoleFonts::get_filesystem(); auto font = fonts.open("CitraSharedFontUSRelocated.bin"); std::memcpy(pointer, font.begin(), font.size()); +} + +std::optional Memory::getProgramID() { + auto cxi = getCXI(); + + if (cxi) { + return cxi->programID; + } + + return std::nullopt; } \ No newline at end of file diff --git a/src/core/services/dsp.cpp b/src/core/services/dsp.cpp index 69eb9fb36..33c1703d1 100644 --- a/src/core/services/dsp.cpp +++ b/src/core/services/dsp.cpp @@ -31,13 +31,8 @@ namespace Result { } void DSPService::reset() { - for (auto& e : pipeData) - e.clear(); - - // Note: Reset audio pipe AFTER resetting all pipes, otherwise the new data will be yeeted - resetAudioPipe(); totalEventCount = 0; - dspState = DSPState::Off; + semaphoreMask = 0; semaphoreEvent = std::nullopt; interrupt0 = std::nullopt; @@ -48,40 +43,6 @@ void DSPService::reset() { } } -void DSPService::resetAudioPipe() { - // Hardcoded responses for now - // These are DSP DRAM offsets for various variables - // https://www.3dbrew.org/wiki/DSP_Memory_Region - static constexpr std::array responses = { - 0x000F, // Number of responses - 0xBFFF, // Frame counter - 0x9E92, // Source configs - 0x8680, // Source statuses - 0xA792, // ADPCM coefficients - 0x9430, // DSP configs - 0x8400, // DSP status - 0x8540, // Final samples - 0x9492, // Intermediate mix samples - 0x8710, // Compressor - 0x8410, // Debug - 0xA912, // ?? - 0xAA12, // ?? - 0xAAD2, // ?? - 0xAC52, // Surround sound biquad filter 1 - 0xAC5C // Surround sound biquad filter 2 - }; - - 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 DSPService::handleSyncRequest(u32 messagePointer) { const u32 command = mem.read32(messagePointer); switch (command) { @@ -117,8 +78,16 @@ void DSPService::loadComponent(u32 messagePointer) { u32 size = mem.read32(messagePointer + 4); u32 programMask = mem.read32(messagePointer + 8); u32 dataMask = mem.read32(messagePointer + 12); + u32 buffer = mem.read32(messagePointer + 20); + + std::vector data(size); + for (u32 i = 0; i < size; i++) { + data[i] = mem.read8(buffer + i); + } log("DSP::LoadComponent (size = %08X, program mask = %X, data mask = %X\n", size, programMask, dataMask); + dsp->loadComponent(data, programMask, dataMask); + mem.write32(messagePointer, IPC::responseHeader(0x11, 2, 2)); mem.write32(messagePointer + 4, Result::Success); mem.write32(messagePointer + 8, 1); // Component loaded @@ -128,32 +97,12 @@ void DSPService::loadComponent(u32 messagePointer) { void DSPService::unloadComponent(u32 messagePointer) { log("DSP::UnloadComponent\n"); + dsp->unloadComponent(); + mem.write32(messagePointer, IPC::responseHeader(0x12, 1, 0)); mem.write32(messagePointer + 4, Result::Success); } -std::vector DSPService::readPipe(u32 pipe, u32 size) { - 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, u32(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 DSPService::readPipeIfPossible(u32 messagePointer) { u32 channel = mem.read32(messagePointer + 4); u32 peer = mem.read32(messagePointer + 8); @@ -162,7 +111,7 @@ void DSPService::readPipeIfPossible(u32 messagePointer) { log("DSP::ReadPipeIfPossible (channel = %d, peer = %d, size = %04X, buffer = %08X)\n", channel, peer, size, buffer); mem.write32(messagePointer, IPC::responseHeader(0x10, 2, 2)); - std::vector data = readPipe(channel, size); + std::vector data = dsp->readPipe(channel, peer, size, buffer); for (uint i = 0; i < data.size(); i++) { mem.write8(buffer + i, data[i]); } @@ -176,22 +125,22 @@ void DSPService::recvData(u32 messagePointer) { log("DSP::RecvData (register = %d)\n", registerIndex); if (registerIndex != 0) Helpers::panic("Unknown register in DSP::RecvData"); - // Return 0 if the DSP is running, otherwise 1 - const u16 ret = dspState == DSPState::On ? 0 : 1; + const u16 data = dsp->recvData(registerIndex); mem.write32(messagePointer, IPC::responseHeader(0x01, 2, 0)); mem.write32(messagePointer + 4, Result::Success); - mem.write16(messagePointer + 8, ret); + mem.write16(messagePointer + 8, data); } void DSPService::recvDataIsReady(u32 messagePointer) { const u32 registerIndex = mem.read32(messagePointer + 4); log("DSP::RecvDataIsReady (register = %d)\n", registerIndex); - if (registerIndex != 0) Helpers::panic("Unknown register in DSP::RecvDataIsReady"); + + bool isReady = dsp->recvDataIsReady(registerIndex); mem.write32(messagePointer, IPC::responseHeader(0x02, 2, 0)); mem.write32(messagePointer + 4, Result::Success); - mem.write32(messagePointer + 8, 1); // Always return that the register is ready for now + mem.write32(messagePointer + 8, isReady ? 1 : 0); } DSPService::DSPEvent& DSPService::getEventRef(u32 type, u32 pipe) { @@ -236,7 +185,6 @@ void DSPService::registerInterruptEvents(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); totalEventCount++; - kernel.signalEvent(eventHandle); } } } @@ -253,7 +201,7 @@ void DSPService::getSemaphoreEventHandle(u32 messagePointer) { log("DSP::GetSemaphoreEventHandle\n"); if (!semaphoreEvent.has_value()) { - semaphoreEvent = kernel.makeEvent(ResetType::OneShot); + semaphoreEvent = kernel.makeEvent(ResetType::OneShot, Event::CallbackType::DSPSemaphore); } mem.write32(messagePointer, IPC::responseHeader(0x16, 1, 2)); @@ -267,6 +215,7 @@ void DSPService::setSemaphore(u32 messagePointer) { const u16 value = mem.read16(messagePointer + 4); log("DSP::SetSemaphore(value = %04X)\n", value); + dsp->setSemaphore(value); mem.write32(messagePointer, IPC::responseHeader(0x7, 1, 0)); mem.write32(messagePointer + 4, Result::Success); } @@ -275,6 +224,9 @@ void DSPService::setSemaphoreMask(u32 messagePointer) { const u16 mask = mem.read16(messagePointer + 4); log("DSP::SetSemaphoreMask(mask = %04X)\n", mask); + dsp->setSemaphoreMask(mask); + semaphoreMask = mask; + mem.write32(messagePointer, IPC::responseHeader(0x17, 1, 0)); mem.write32(messagePointer + 4, Result::Success); } @@ -285,51 +237,7 @@ void DSPService::writeProcessPipe(u32 messagePointer) { const u32 buffer = mem.read32(messagePointer + 16); log("DSP::writeProcessPipe (channel = %d, size = %X, buffer = %08X)\n", channel, size, 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(); - 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); - break; - - default: - log("DSP: Wrote to unimplemented pipe %d\n", channel); - break; - } - + dsp->writeProcessPipe(channel, size, buffer); mem.write32(messagePointer, IPC::responseHeader(0xD, 1, 0)); mem.write32(messagePointer + 4, Result::Success); } @@ -354,12 +262,26 @@ void DSPService::invalidateDCache(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } -void DSPService::signalEvents() { - for (const DSPEvent& e : pipeEvents) { - if (e.has_value()) { kernel.signalEvent(e.value()); } +void DSPService::triggerPipeEvent(int index) { + if (index < pipeCount && pipeEvents[index].has_value()) { + kernel.signalEvent(*pipeEvents[index]); } +} + +void DSPService::triggerSemaphoreEvent() { + if (semaphoreEvent.has_value()) { + kernel.signalEvent(*semaphoreEvent); + } +} - if (semaphoreEvent.has_value()) { kernel.signalEvent(semaphoreEvent.value()); } - if (interrupt0.has_value()) { kernel.signalEvent(interrupt0.value()); } - if (interrupt1.has_value()) { kernel.signalEvent(interrupt1.value()); } +void DSPService::triggerInterrupt0() { + if (interrupt0.has_value()) { + kernel.signalEvent(*interrupt0); + } +} + +void DSPService::triggerInterrupt1() { + if (interrupt1.has_value()) { + kernel.signalEvent(*interrupt1); + } } \ No newline at end of file diff --git a/src/core/services/gsp_gpu.cpp b/src/core/services/gsp_gpu.cpp index d77f936ef..2e1ce2d32 100644 --- a/src/core/services/gsp_gpu.cpp +++ b/src/core/services/gsp_gpu.cpp @@ -123,10 +123,6 @@ void GPUService::registerInterruptRelayQueue(u32 messagePointer) { } void GPUService::requestInterrupt(GPUInterrupt type) { - // HACK: Signal DSP events on GPU interrupt for now until we have the DSP since games need DSP events - // Maybe there's a better alternative? - kernel.signalDSPEvents(); - if (sharedMem == nullptr) [[unlikely]] { // Shared memory hasn't been set up yet return; } diff --git a/src/core/services/nwm_uds.cpp b/src/core/services/nwm_uds.cpp index 9c0ef95f1..7752e5030 100644 --- a/src/core/services/nwm_uds.cpp +++ b/src/core/services/nwm_uds.cpp @@ -37,7 +37,8 @@ void NwmUdsService::initializeWithVersion(u32 messagePointer) { initialized = true; - mem.write32(messagePointer + 4, Result::Success); + // Stubbed to fail temporarily, since some games will break trying to establish networks otherwise + mem.write32(messagePointer + 4, Result::FailurePlaceholder); mem.write32(messagePointer + 8, 0); mem.write32(messagePointer + 12, eventHandle.value()); } diff --git a/src/emulator.cpp b/src/emulator.cpp index c567cbc7a..a02ead482 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -18,12 +18,20 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1; Emulator::Emulator() : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config), - cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false) + cheats(memory, kernel.getServiceManager().getHID()), lua(*this), running(false), programRunning(false) #ifdef PANDA3DS_ENABLE_HTTP_SERVER , httpServer(this) #endif { + DSPService& dspService = kernel.getServiceManager().getDSP(); + + dsp = Audio::makeDSPCore(config.dspType, memory, scheduler, dspService); + dspService.setDSPCore(dsp.get()); + + audioDevice.init(dsp->getSamples()); + setAudioEnabled(config.audioEnabled); + #ifdef PANDA3DS_ENABLE_DISCORD_RPC if (config.discordRpcEnabled) { discordRpc.init(); @@ -46,6 +54,8 @@ void Emulator::reset(ReloadOption reload) { cpu.reset(); gpu.reset(); memory.reset(); + dsp->reset(); + // Reset scheduler and add a VBlank event scheduler.reset(); @@ -95,8 +105,19 @@ void Emulator::step() {} void Emulator::render() {} // Only resume if a ROM is properly loaded -void Emulator::resume() { running = (romType != ROMType::None); } -void Emulator::pause() { running = false; } +void Emulator::resume() { + running = (romType != ROMType::None); + + if (running && config.audioEnabled) { + audioDevice.start(); + } +} + +void Emulator::pause() { + running = false; + audioDevice.stop(); +} + void Emulator::togglePause() { running ? pause() : resume(); } void Emulator::runFrame() { @@ -136,13 +157,17 @@ void Emulator::pollScheduler() { ServiceManager& srv = kernel.getServiceManager(); srv.sendGPUInterrupt(GPUInterrupt::VBlank0); srv.sendGPUInterrupt(GPUInterrupt::VBlank1); - + // Queue next VBlank event scheduler.addEvent(Scheduler::EventType::VBlank, time + CPU::ticksPerSec / 60); break; } case Scheduler::EventType::UpdateTimers: kernel.pollTimers(); break; + case Scheduler::EventType::RunDSP: { + dsp->runAudioFrame(); + break; + } default: { Helpers::panic("Scheduler: Unimplemented event type received: %d\n", static_cast(eventType)); @@ -376,4 +401,16 @@ RomFS::DumpingResult Emulator::dumpRomFS(const std::filesystem::path& path) { dumpRomFSNode(*node, (const char*)&romFS[0], path); return DumpingResult::Success; -} \ No newline at end of file +} + +void Emulator::setAudioEnabled(bool enable) { + if (!enable) { + audioDevice.stop(); + } else if (enable && romType != ROMType::None && running) { + // Don't start the audio device yet if there's no ROM loaded or the emulator is paused + // Resume and Pause will handle it + audioDevice.start(); + } + + dsp->setAudioEnabled(enable); +} diff --git a/src/lua.cpp b/src/lua.cpp index 09c631735..d12faf7ee 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -1,4 +1,5 @@ #ifdef PANDA3DS_ENABLE_LUA +#include "emulator.hpp" #include "lua_manager.hpp" #ifndef __ANDROID__ @@ -42,7 +43,7 @@ void LuaManager::loadFile(const char* path) { if (!initialized) { initialize(); } - + // If init failed, don't execute if (!initialized) { printf("Lua initialization failed, file won't run\n"); @@ -88,8 +89,8 @@ void LuaManager::loadString(const std::string& code) { } void LuaManager::signalEventInternal(LuaEvent e) { - lua_getglobal(L, "eventHandler"); // We want to call the event handler - lua_pushnumber(L, static_cast(e)); // Push event type + lua_getglobal(L, "eventHandler"); // We want to call the event handler + lua_pushnumber(L, static_cast(e)); // Push event type // Call the function with 1 argument and 0 outputs, without an error handler lua_pcall(L, 1, 0, 0); @@ -103,21 +104,20 @@ void LuaManager::reset() { // Initialize C++ thunks for Lua code to call here // All code beyond this point is terrible and full of global state, don't judge -Memory* LuaManager::g_memory = nullptr; - -#define MAKE_MEMORY_FUNCTIONS(size) \ - static int read##size##Thunk(lua_State* L) { \ - const u32 vaddr = (u32)lua_tonumber(L, 1); \ - lua_pushnumber(L, LuaManager::g_memory->read##size(vaddr)); \ - return 1; \ - } \ - static int write##size##Thunk(lua_State* L) { \ - const u32 vaddr = (u32)lua_tonumber(L, 1); \ - const u##size value = (u##size)lua_tonumber(L, 2); \ - LuaManager::g_memory->write##size(vaddr, value); \ - return 0; \ - } +Emulator* LuaManager::g_emulator = nullptr; +#define MAKE_MEMORY_FUNCTIONS(size) \ + static int read##size##Thunk(lua_State* L) { \ + const u32 vaddr = (u32)lua_tonumber(L, 1); \ + lua_pushnumber(L, LuaManager::g_emulator->getMemory().read##size(vaddr)); \ + return 1; \ + } \ + static int write##size##Thunk(lua_State* L) { \ + const u32 vaddr = (u32)lua_tonumber(L, 1); \ + const u##size value = (u##size)lua_tonumber(L, 2); \ + LuaManager::g_emulator->getMemory().write##size(vaddr, value); \ + return 0; \ + } MAKE_MEMORY_FUNCTIONS(8) MAKE_MEMORY_FUNCTIONS(16) @@ -125,6 +125,84 @@ MAKE_MEMORY_FUNCTIONS(32) MAKE_MEMORY_FUNCTIONS(64) #undef MAKE_MEMORY_FUNCTIONS +static int getAppIDThunk(lua_State* L) { + std::optional id = LuaManager::g_emulator->getMemory().getProgramID(); + + // If the app has an ID, return true + its ID + // Otherwise return false and 0 as the ID + if (id.has_value()) { + lua_pushboolean(L, 1); // Return true + lua_pushnumber(L, u32(*id)); // Return bottom 32 bits + lua_pushnumber(L, u32(*id >> 32)); // Return top 32 bits + } else { + lua_pushboolean(L, 0); // Return false + // Return no ID + lua_pushnumber(L, 0); + lua_pushnumber(L, 0); + } + + return 3; +} + +static int pauseThunk(lua_State* L) { + LuaManager::g_emulator->pause(); + return 0; +} + +static int resumeThunk(lua_State* L) { + LuaManager::g_emulator->resume(); + return 0; +} + +static int resetThunk(lua_State* L) { + LuaManager::g_emulator->reset(Emulator::ReloadOption::Reload); + return 0; +} + +static int loadROMThunk(lua_State* L) { + // Path argument is invalid, report that loading failed and exit + if (lua_type(L, -1) != LUA_TSTRING) { + lua_pushboolean(L, 0); + return 1; + } + + size_t pathLength; + const char* const str = lua_tolstring(L, -1, &pathLength); + + const auto path = std::filesystem::path(std::string(str, pathLength)); + // Load ROM and reply if it succeeded or not + lua_pushboolean(L, LuaManager::g_emulator->loadROM(path) ? 1 : 0); + return 1; +} + +static int getButtonsThunk(lua_State* L) { + auto buttons = LuaManager::g_emulator->getServiceManager().getHID().getOldButtons(); + lua_pushinteger(L, static_cast(buttons)); + + return 1; +} + +static int getCirclepadThunk(lua_State* L) { + auto& hid = LuaManager::g_emulator->getServiceManager().getHID(); + s16 x = hid.getCirclepadX(); + s16 y = hid.getCirclepadY(); + + lua_pushinteger(L, static_cast(x)); + lua_pushinteger(L, static_cast(y)); + return 2; +} + +static int getButtonThunk(lua_State* L) { + auto& hid = LuaManager::g_emulator->getServiceManager().getHID(); + // This function accepts a mask. You can use it to check if one or more buttons are pressed at a time + const u32 mask = (u32)lua_tonumber(L, 1); + const bool result = (hid.getOldButtons() & mask) == mask; + + // Return whether the selected buttons are all pressed + lua_pushboolean(L, result ? 1 : 0); + return 1; +} + // clang-format off static constexpr luaL_Reg functions[] = { { "__read8", read8Thunk }, @@ -135,6 +213,14 @@ static constexpr luaL_Reg functions[] = { { "__write16", write16Thunk }, { "__write32", write32Thunk }, { "__write64", write64Thunk }, + { "__getAppID", getAppIDThunk }, + { "__pause", pauseThunk}, + { "__resume", resumeThunk}, + { "__reset", resetThunk}, + { "__loadROM", loadROMThunk}, + { "__getButtons", getButtonsThunk}, + { "__getCirclepad", getCirclepadThunk}, + { "__getButton", getButtonThunk}, { nullptr, nullptr }, }; // clang-format on @@ -150,7 +236,35 @@ void LuaManager::initializeThunks() { write16 = function(addr, value) GLOBALS.__write16(addr, value) end, write32 = function(addr, value) GLOBALS.__write32(addr, value) end, write64 = function(addr, value) GLOBALS.__write64(addr, value) end, + + getAppID = function() + local ffi = require("ffi") + + result, low, high = GLOBALS.__getAppID() + id = bit.bor(ffi.cast("uint64_t", low), (bit.lshift(ffi.cast("uint64_t", high), 32))) + return result, id + end, + + pause = function() GLOBALS.__pause() end, + resume = function() GLOBALS.__resume() end, + reset = function() GLOBALS.__reset() end, + loadROM = function(path) return GLOBALS.__loadROM(path) end, + + getButtons = function() return GLOBALS.__getButtons() end, + getButton = function(button) return GLOBALS.__getButton(button) end, + getCirclepad = function() return GLOBALS.__getCirclepad() end, + Frame = __Frame, + ButtonA = __ButtonA, + ButtonB = __ButtonB, + ButtonX = __ButtonX, + ButtonY = __ButtonY, + ButtonL = __ButtonL, + ButtonR = __ButtonR, + ButtonUp = __ButtonUp, + ButtonDown = __ButtonDown, + ButtonLeft = __ButtonLeft, + ButtonRight= __ButtonRight, } )"; @@ -160,8 +274,21 @@ void LuaManager::initializeThunks() { }; luaL_register(L, "GLOBALS", functions); + // Add values for event enum addIntConstant(LuaEvent::Frame, "__Frame"); + // Add enums for 3DS keys + addIntConstant(HID::Keys::A, "__ButtonA"); + addIntConstant(HID::Keys::B, "__ButtonB"); + addIntConstant(HID::Keys::X, "__ButtonX"); + addIntConstant(HID::Keys::Y, "__ButtonY"); + addIntConstant(HID::Keys::Up, "__ButtonUp"); + addIntConstant(HID::Keys::Down, "__ButtonDown"); + addIntConstant(HID::Keys::Left, "__ButtonLeft"); + addIntConstant(HID::Keys::Right, "__ButtonRight"); + addIntConstant(HID::Keys::L, "__ButtonL"); + addIntConstant(HID::Keys::R, "__ButtonR"); + // Call our Lua runtime initialization before any Lua script runs luaL_loadstring(L, runtimeInit); int ret = lua_pcall(L, 0, 0, 0); // tell Lua to run the script @@ -174,4 +301,4 @@ void LuaManager::initializeThunks() { } } -#endif \ No newline at end of file +#endif diff --git a/src/miniaudio.cpp b/src/miniaudio.cpp new file mode 100644 index 000000000..e42fea683 --- /dev/null +++ b/src/miniaudio.cpp @@ -0,0 +1,7 @@ +// We do not need the ability to be able to encode or decode audio files for the time being +// So we disable said functionality to make the executable smaller +#define MA_NO_DECODING +#define MA_NO_ENCODING +#define MINIAUDIO_IMPLEMENTATION + +#include "miniaudio.h" \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index de70cc187..dff4c1713 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -87,7 +87,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) // Make GL context current for this thread, enable VSync GL::Context* glContext = screen.getGLContext(); glContext->MakeCurrent(); - glContext->SetSwapInterval(1); + glContext->SetSwapInterval(emu->getConfig().vsyncEnabled ? 1 : 0); emu->initGraphicsContext(glContext); } else if (usingVk) { diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 29b1dcb8d..04b582e1c 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -49,6 +49,8 @@ FrontendSDL::FrontendSDL() { if (!gladLoadGLLoader(reinterpret_cast(SDL_GL_GetProcAddress))) { Helpers::panic("OpenGL init failed"); } + + SDL_GL_SetSwapInterval(config.vsyncEnabled ? 1 : 0); } #ifdef PANDA3DS_ENABLE_VULKAN diff --git a/src/pandroid/app/build.gradle.kts b/src/pandroid/app/build.gradle.kts index 201d5db13..b67f94198 100644 --- a/src/pandroid/app/build.gradle.kts +++ b/src/pandroid/app/build.gradle.kts @@ -22,8 +22,8 @@ android { buildTypes { getByName("release") { - isMinifyEnabled = false - isShrinkResources = false + isMinifyEnabled = true + isShrinkResources = true isDebuggable = false signingConfig = signingConfigs.getByName("debug") proguardFiles( diff --git a/src/pandroid/app/proguard-rules.pro b/src/pandroid/app/proguard-rules.pro index 481bb4348..31c24c5af 100644 --- a/src/pandroid/app/proguard-rules.pro +++ b/src/pandroid/app/proguard-rules.pro @@ -1,16 +1,19 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# +# Pandroid Proguard Rules # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +# Keep all JNI and C++ related classes and methods +-keepclasseswithmembernames class * { + native ; +} + +# Keep all native libraries and their methods +-keep class * { + native ; +} + +# Keep all classes in the specified package and its subpackages +-keep class com.panda3ds.pandroid.** {*;} # Uncomment this to preserve the line number information for # debugging stack traces. @@ -18,4 +21,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java index 76dc5e7dc..ee92c0bc9 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlRenderer.java @@ -31,6 +31,14 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer public int screenFbo; private final Context context; + private int shaderProgram; + private int inputTextureLocation; + private int texelSizeLocation; + private int screenSizeLocation; + + private String VertexShaderCode; + private String FragmentShaderCode; + PandaGlRenderer(Context context, String romPath) { super(); this.context = context; @@ -67,6 +75,10 @@ public void onSurfaceCreated(GL10 unused, EGLConfig config) { Log.e(Constants.LOG_TAG, "OpenGL 3.1 or higher is required"); } + // Load and compile shader + loadShaders(); + compileShaderProgram(); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); @@ -149,11 +161,98 @@ public void onDrawFrame(GL10 unused) { 40, Constants.N3DS_HALF_HEIGHT, Constants.N3DS_WIDTH - 40, 0, bottomScreen.left, screenHeight - bottomScreen.top, bottomScreen.right, screenHeight - bottomScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR ); + // Apply Shader effect + applyShader(); } PerformanceMonitor.runFrame(); } +// Load the shader code +private void loadShaders() { +// vertex shader code + VertexShaderCode = "#version 330 core\n" + + "layout(location = 0) in vec3 aPos;\n" + + "void main() {\n" + + " gl_Position = vec4(aPos, 1.0);\n" + + "}\n"; + + // fragment shader code with improved brightness, contrast, and gamma correction + FragmentShaderCode = "#version 330 core\n" + + "uniform sampler2D inputTexture;\n" + + "uniform float brightness;\n" + + "uniform float contrast;\n" + + "uniform float gamma;\n" + + "in vec2 TexCoords;\n" + + "out vec4 FragColor;\n" + + "void main() {\n" + + " vec4 texColor = texture(inputTexture, TexCoords);\n" + + " texColor.rgb *= brightness;\n" + // Apply brightness adjustment directly to RGB components + " texColor.rgb = pow(texColor.rgb, vec3(1.0 / gamma));\n" + // Apply gamma correction + " texColor.rgb = (texColor.rgb - 0.5) * contrast + 0.5;\n" + // Apply contrast adjustment + " FragColor = texColor;\n" + + "}\n"; +} + +// Method to check the compilation status of a shader +private boolean checkShaderCompileStatus(int shader) { + final int[] compileStatus = new int[1]; + glGetShaderiv(shader, GL_COMPILE_STATUS, compileStatus, 0); + if (compileStatus[0] == 0) { + Log.e(Constants.LOG_TAG, "Shader compilation failed: " + glGetShaderInfoLog(shader)); + glDeleteShader(shader); + return false; + } + return true; +} + +// Method to check the linking status of a shader program +private boolean checkProgramLinkStatus(int program) { + final int[] linkStatus = new int[1]; + glGetProgramiv(program, GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + Log.e(Constants.LOG_TAG, "Shader program linking failed: " + glGetProgramInfoLog(program)); + glDeleteProgram(program); + return false; + } + return true; +} + +// Compile and link the shader program +private void compileShaderProgram() { + int vertexShader = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertexShader, VertexShaderCode); + glCompileShader(vertexShader); + checkShaderCompileStatus(vertexShader); + + int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragmentShader, FragmentShaderCode); + glCompileShader(fragmentShader); + checkShaderCompileStatus(fragmentShader); + + shaderProgram = glCreateProgram(); + glAttachShader(shaderProgram, vertexShader); + glAttachShader(shaderProgram, fragmentShader); + glLinkProgram(shaderProgram); + checkProgramLinkStatus(shaderProgram); + + // Get uniform locations + inputTextureLocation = glGetUniformLocation(shaderProgram, "inputTexture"); + texelSizeLocation = glGetUniformLocation(shaderProgram, "texelSize"); + screenSizeLocation = glGetUniformLocation(shaderProgram, "screenSize"); +} + +// Render a fullscreen quad with the FXAA shader +private void applyShader() { + glUseProgram(shaderProgram); + + // Set shader uniforms + glUniform1i(inputTextureLocation, 0); // Assuming input texture is bound to texture unit 0 + glUniform2f(texelSizeLocation, 1.0f / screenWidth, 1.0f / screenHeight); + glUniform2f(screenSizeLocation, screenWidth, screenHeight); + +} + public void onSurfaceChanged(GL10 unused, int width, int height) { screenWidth = width; screenHeight = height; diff --git a/third_party/miniaudio b/third_party/miniaudio new file mode 160000 index 000000000..4a5b74bef --- /dev/null +++ b/third_party/miniaudio @@ -0,0 +1 @@ +Subproject commit 4a5b74bef029b3592c54b6048650ee5f972c1a48 diff --git a/third_party/teakra b/third_party/teakra new file mode 160000 index 000000000..01db7cdd0 --- /dev/null +++ b/third_party/teakra @@ -0,0 +1 @@ +Subproject commit 01db7cdd00aabcce559a8dddce8798dabb71949b