diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml index 137577c1..2d0fd844 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 f1e8f469..77a6bc6a 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 81572703..22047345 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/PICA/shader_gen.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/android_utils.hpp b/include/android_utils.hpp new file mode 100644 index 00000000..0e1a016a --- /dev/null +++ b/include/android_utils.hpp @@ -0,0 +1,5 @@ +#pragma once + +namespace AndroidUtils { + int openDocument(const char* directory, const char* mode); +} \ No newline at end of file diff --git a/include/audio/dsp_core.hpp b/include/audio/dsp_core.hpp new file mode 100644 index 00000000..1a556f28 --- /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 00000000..f4d126d8 --- /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 00000000..7d6f1c9e --- /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 00000000..6a011231 --- /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 155f5961..8c0d2e12 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/cpu_dynarmic.hpp b/include/cpu_dynarmic.hpp index 048bc62a..43f31d30 100644 --- a/include/cpu_dynarmic.hpp +++ b/include/cpu_dynarmic.hpp @@ -17,7 +17,6 @@ class CPU; class MyEnvironment final : public Dynarmic::A32::UserCallbacks { public: u64 ticksLeft = 0; - u64 totalTicks = 0; Memory& mem; Kernel& kernel; Scheduler& scheduler; diff --git a/include/emulator.hpp b/include/emulator.hpp index d3377f6c..47fbc839 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,10 +44,13 @@ class Emulator { GPU gpu; Memory memory; Kernel kernel; - Crypto::AESEngine aesEngine; - Cheats cheats; + std::unique_ptr dsp; Scheduler scheduler; + Crypto::AESEngine aesEngine; + MiniAudioDevice audioDevice; + Cheats cheats; + // 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 // And so the user can still use the keyboard to control the analog @@ -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 e78a588a..fc7fe3f3 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 @@ public: 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 01af4bd9..a68ef8d5 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 c5ef2465..42ce1590 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 e021a685..4fc521b6 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 50b8dd61..46fd553a 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 640ae5f0..1b6e622c 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 @@ private: // 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 00000000..35d7d935 --- /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 5645f47d..97c50afc 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 ab9fb106..bc27377d 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 @@ public: 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 @@ public: 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 93700498..8d1cf381 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 cd4e1f79..f19ff06d 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/CPU/cpu_dynarmic.cpp b/src/core/CPU/cpu_dynarmic.cpp index da5270b4..85dc70d9 100644 --- a/src/core/CPU/cpu_dynarmic.cpp +++ b/src/core/CPU/cpu_dynarmic.cpp @@ -21,7 +21,6 @@ CPU::CPU(Memory& mem, Kernel& kernel, Emulator& emu) : mem(mem), emu(emu), sched void CPU::reset() { setCPSR(CPSR::UserMode); setFPSCR(FPSCR::MainThreadDefault); - env.totalTicks = 0; cp15->reset(); cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage diff --git a/src/core/audio/dsp_core.cpp b/src/core/audio/dsp_core.cpp new file mode 100644 index 00000000..e4162e93 --- /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 00000000..fa36cb84 --- /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 00000000..ec073ae7 --- /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 00000000..da2e5a5a --- /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 b2f89fbf..7c0d3047 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 1f534fa0..3bf73e5d 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 83248ef5..09b49eee 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/apt.cpp b/src/core/services/apt.cpp index 404a0e59..ddeb18de 100644 --- a/src/core/services/apt.cpp +++ b/src/core/services/apt.cpp @@ -84,7 +84,7 @@ void APTService::appletUtility(u32 messagePointer) { u32 outputSize = mem.read32(messagePointer + 12); u32 inputPointer = mem.read32(messagePointer + 20); - log("APT::AppletUtility(utility = %d, input size = %x, output size = %x, inputPointer = %08X) (Stubbed)\n", utility, inputSize, outputSize, + log("APT::AppletUtility(utility = %d, input size = %x, output size = %x, inputPointer = %08X)\n", utility, inputSize, outputSize, inputPointer); std::vector out(outputSize); @@ -218,7 +218,7 @@ void APTService::initialize(u32 messagePointer) { } void APTService::inquireNotification(u32 messagePointer) { - log("APT::InquireNotification (STUBBED TO RETURN NONE)\n"); + log("APT::InquireNotification\n"); mem.write32(messagePointer, IPC::responseHeader(0xB, 2, 0)); mem.write32(messagePointer + 4, Result::Success); diff --git a/src/core/services/dsp.cpp b/src/core/services/dsp.cpp index 69eb9fb3..33c1703d 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]); } +} - 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::triggerSemaphoreEvent() { + if (semaphoreEvent.has_value()) { + kernel.signalEvent(*semaphoreEvent); + } +} + +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 8dff6a79..2e1ce2d3 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; } @@ -289,7 +285,7 @@ void GPUService::setBufferSwap(u32 messagePointer) { info.displayFb = mem.read32(messagePointer + 28); // Selects either framebuffer A or B log("GSP::GPU::SetBufferSwap\n"); - Helpers::panic("Untested GSP::GPU::SetBufferSwap call"); + Helpers::warn("Untested GSP::GPU::SetBufferSwap call"); setBufferSwapImpl(screenId, info); mem.write32(messagePointer, IPC::responseHeader(0x05, 1, 0)); @@ -529,4 +525,4 @@ void GPUService::importDisplayCaptureInfo(u32 messagePointer) { mem.write32(messagePointer + 28, bottomScreenCapture.rightFramebuffer); mem.write32(messagePointer + 32, bottomScreenCapture.format); mem.write32(messagePointer + 36, bottomScreenCapture.stride); -} \ No newline at end of file +} diff --git a/src/core/services/nwm_uds.cpp b/src/core/services/nwm_uds.cpp index 9c0ef95f..7752e503 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 c567cbc7..a02ead48 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/io_file.cpp b/src/io_file.cpp index 1f794284..3bfac013 100644 --- a/src/io_file.cpp +++ b/src/io_file.cpp @@ -21,6 +21,10 @@ #include // For ftruncate #endif +#ifdef __ANDROID__ +#include "android_utils.hpp" +#endif + IOFile::IOFile(const std::filesystem::path& path, const char* permissions) : handle(nullptr) { open(path, permissions); } bool IOFile::open(const std::filesystem::path& path, const char* permissions) { @@ -34,8 +38,19 @@ bool IOFile::open(const char* filename, const char* permissions) { if (isOpen()) { close(); } + #ifdef __ANDROID__ + std::string path(filename); + + // Check if this is a URI directory, which will need special handling due to SAF + if (path.find("://") != std::string::npos ) { + handle = fdopen(AndroidUtils::openDocument(filename, permissions), permissions); + } else { + handle = std::fopen(filename, permissions); + } + #else + handle = std::fopen(filename, permissions); + #endif - handle = std::fopen(filename, permissions); return isOpen(); } diff --git a/src/jni_driver.cpp b/src/jni_driver.cpp index d962f23e..e4ce2b39 100644 --- a/src/jni_driver.cpp +++ b/src/jni_driver.cpp @@ -7,6 +7,7 @@ #include "emulator.hpp" #include "renderer_gl/renderer_gl.hpp" #include "services/hid.hpp" +#include "android_utils.hpp" std::unique_ptr emulator = nullptr; HIDService* hidService = nullptr; @@ -14,6 +15,9 @@ RendererGL* renderer = nullptr; bool romLoaded = false; JavaVM* jvm = nullptr; +jclass alberClass; +jmethodID alberClassOpenDocument; + #define AlberFunction(type, name) JNIEXPORT type JNICALL Java_com_panda3ds_pandroid_AlberDriver_##name void throwException(JNIEnv* env, const char* message) { @@ -42,7 +46,13 @@ MAKE_SETTING(setShaderJitEnabled, jboolean, shaderJitEnabled) #undef MAKE_SETTING -AlberFunction(void, Setup)(JNIEnv* env, jobject obj) { env->GetJavaVM(&jvm); } +AlberFunction(void, Setup)(JNIEnv* env, jobject obj) { + env->GetJavaVM(&jvm); + + alberClass = (jclass)env->NewGlobalRef((jclass)env->FindClass("com/panda3ds/pandroid/AlberDriver")); + alberClassOpenDocument = env->GetStaticMethodID(alberClass, "openDocument", "(Ljava/lang/String;Ljava/lang/String;)I"); +} + AlberFunction(void, Pause)(JNIEnv* env, jobject obj) { emulator->pause(); } AlberFunction(void, Resume)(JNIEnv* env, jobject obj) { emulator->resume(); } @@ -81,10 +91,12 @@ AlberFunction(void, Finalize)(JNIEnv* env, jobject obj) { AlberFunction(jboolean, HasRomLoaded)(JNIEnv* env, jobject obj) { return romLoaded; } -AlberFunction(void, LoadRom)(JNIEnv* env, jobject obj, jstring path) { +AlberFunction(jboolean, LoadRom)(JNIEnv* env, jobject obj, jstring path) { const char* pathStr = env->GetStringUTFChars(path, nullptr); romLoaded = emulator->loadROM(pathStr); env->ReleaseStringUTFChars(path, pathStr); + + return romLoaded; } AlberFunction(void, LoadLuaScript)(JNIEnv* env, jobject obj, jstring script) { @@ -114,3 +126,17 @@ AlberFunction(jbyteArray, GetSmdh)(JNIEnv* env, jobject obj) { } #undef AlberFunction + +int AndroidUtils::openDocument(const char* path, const char* perms) { + auto env = jniEnv(); + + jstring uri = env->NewStringUTF(path); + jstring jmode = env->NewStringUTF(perms); + + jint result = env->CallStaticIntMethod(alberClass, alberClassOpenDocument, uri, jmode); + + env->DeleteLocalRef(uri); + env->DeleteLocalRef(jmode); + + return (int)result; +} \ No newline at end of file diff --git a/src/lua.cpp b/src/lua.cpp index 09c63173..d12faf7e 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,28 +104,105 @@ 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; +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_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; \ +#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) 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 00000000..e42fea68 --- /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 de70cc18..dff4c171 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 29b1dcb8..04b582e1 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 201d5db1..b67f9419 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 481bb434..31c24c5a 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/AndroidManifest.xml b/src/pandroid/app/src/main/AndroidManifest.xml index c66d37af..1deae1a4 100644 --- a/src/pandroid/app/src/main/AndroidManifest.xml +++ b/src/pandroid/app/src/main/AndroidManifest.xml @@ -2,10 +2,6 @@ - - - - @@ -24,6 +20,10 @@ android:hardwareAccelerated="true" android:theme="@style/Theme.Pandroid" tools:targetApi="31"> + + + = Build.VERSION_CODES.O_MR1) { + getTheme().applyStyle(R.style.GameActivityNavigationBar, true); + } } @Override diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java index 18914a80..5e03e516 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/MainActivity.java @@ -1,32 +1,18 @@ package com.panda3ds.pandroid.app; -import static android.Manifest.permission.READ_EXTERNAL_STORAGE; -import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; -import static android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION; - -import android.content.Intent; -import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.view.MenuItem; import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; + import com.google.android.material.navigation.NavigationBarView; import com.panda3ds.pandroid.R; -import com.panda3ds.pandroid.app.editor.CodeEditorActivity; import com.panda3ds.pandroid.app.main.GamesFragment; import com.panda3ds.pandroid.app.main.SearchFragment; import com.panda3ds.pandroid.app.main.SettingsFragment; -import java.io.File; - - public class MainActivity extends BaseActivity implements NavigationBarView.OnItemSelectedListener { - private static final int PICK_ROM = 2; - private static final int PERMISSION_REQUEST_CODE = 3; - private final GamesFragment gamesFragment = new GamesFragment(); private final SearchFragment searchFragment = new SearchFragment(); private final SettingsFragment settingsFragment = new SettingsFragment(); @@ -34,16 +20,6 @@ public class MainActivity extends BaseActivity implements NavigationBarView.OnIt @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (!Environment.isExternalStorageManager()) { - Intent intent = new Intent(ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); - startActivity(intent); - } - } else { - ActivityCompat.requestPermissions(this, new String[] {READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE); - ActivityCompat.requestPermissions(this, new String[] {WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE); - } - setContentView(R.layout.activity_main); NavigationBarView bar = findViewById(R.id.navigation); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java index 9482df1d..4f5c5761 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BasePreferenceFragment.java @@ -3,6 +3,7 @@ package com.panda3ds.pandroid.app.base; import android.annotation.SuppressLint; import androidx.annotation.StringRes; +import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; @@ -22,6 +23,9 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { } protected void setActivityTitle(@StringRes int titleId) { - ((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(titleId); + ActionBar header = ((AppCompatActivity) requireActivity()).getSupportActionBar(); + if (header != null) { + header.setTitle(titleId); + } } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/LoadingAlertDialog.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/LoadingAlertDialog.java new file mode 100644 index 00000000..61887fb9 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/LoadingAlertDialog.java @@ -0,0 +1,22 @@ +package com.panda3ds.pandroid.app.base; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.appcompat.widget.AppCompatTextView; + +import com.panda3ds.pandroid.R; + +public class LoadingAlertDialog extends BottomAlertDialog { + public LoadingAlertDialog(@NonNull Context context, @StringRes int title) { + super(context); + View view = LayoutInflater.from(context).inflate(R.layout.dialog_loading,null, false); + setView(view); + setCancelable(false); + ((AppCompatTextView)view.findViewById(R.id.title)) + .setText(title); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java index bd402b52..a1fa9eec 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/DrawerFragment.java @@ -12,6 +12,8 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatTextView; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import com.google.android.material.navigation.NavigationView; import com.panda3ds.pandroid.AlberDriver; @@ -46,8 +48,7 @@ public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListe ((AppCompatTextView)view.findViewById(R.id.game_title)).setText(game.getTitle()); ((AppCompatTextView)view.findViewById(R.id.game_publisher)).setText(game.getPublisher()); - ((NavigationView)view.findViewById(R.id.action_navigation)).setNavigationItemSelectedListener(this); - ((NavigationView)view.findViewById(R.id.others_navigation)).setNavigationItemSelectedListener(this); + ((NavigationView)view.findViewById(R.id.menu)).setNavigationItemSelectedListener(this); } @Override @@ -104,8 +105,11 @@ public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListe close(); } else if (id == R.id.exit) { requireActivity().finish(); - } else if (id == R.id.lua_script){ + } else if (id == R.id.lua_script) { new LuaDialogFragment().show(getParentFragmentManager(), null); + } else if (id == R.id.change_orientation) { + boolean isLandscape = getResources().getDisplayMetrics().widthPixels > getResources().getDisplayMetrics().heightPixels; + requireActivity().setRequestedOrientation(isLandscape ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); } return false; diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java index 1db9f9c7..1d573e42 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/game/LuaDialogFragment.java @@ -19,6 +19,7 @@ import com.panda3ds.pandroid.app.base.BottomAlertDialog; import com.panda3ds.pandroid.app.base.BottomDialogFragment; import com.panda3ds.pandroid.app.editor.CodeEditorActivity; import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.Constants; import com.panda3ds.pandroid.utils.FileUtils; import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout; import com.panda3ds.pandroid.view.recycler.SimpleListAdapter; @@ -94,9 +95,8 @@ public class LuaDialogFragment extends BottomDialogFragment { ((RecyclerView) view.findViewById(R.id.recycler)).setAdapter(adapter); ((RecyclerView) view.findViewById(R.id.recycler)).setLayoutManager(new AutoFitGridLayout(getContext(), 140)); - FileUtils.createDir(FileUtils.getResourcesPath(), "Lua Scripts"); ArrayList files = new ArrayList<>(); - String path = FileUtils.getResourcesPath() + "/Lua Scripts/"; + String path = FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_LUA_SCRIPTS); for (String file : FileUtils.listFiles(path)) { files.add(new LuaFile(file)); } @@ -167,7 +167,7 @@ public class LuaDialogFragment extends BottomDialogFragment { } private LuaFile(String name) { - this(FileUtils.getResourcesPath() + "/Lua Scripts/", name); + this(FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_LUA_SCRIPTS), name); } private String absolutePath() { @@ -182,4 +182,4 @@ public class LuaDialogFragment extends BottomDialogFragment { return FileUtils.getLastModified(absolutePath()); } } -} \ No newline at end of file +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java index ff6e4dca..a5c673f5 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/GamesFragment.java @@ -11,13 +11,17 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.base.LoadingAlertDialog; import com.panda3ds.pandroid.data.game.GameMetadata; +import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.Constants; import com.panda3ds.pandroid.utils.FileUtils; import com.panda3ds.pandroid.utils.GameUtils; import com.panda3ds.pandroid.view.gamesgrid.GamesGridView; - +import java.util.UUID; public class GamesFragment extends Fragment implements ActivityResultCallback { private final ActivityResultContracts.OpenDocument openRomContract = new ActivityResultContracts.OpenDocument(); @@ -49,18 +53,45 @@ public class GamesFragment extends Fragment implements ActivityResultCallback { + String uuid = UUID.randomUUID().toString() + "." + FileUtils.extension(uri); + String name = FileUtils.getName(uri); + FileUtils.copyFile(uri, FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_ELF), uuid); + gameListView.post(() -> { + dialog.hide(); + GameMetadata game = new GameMetadata("elf://" + uuid, name.substring(0, name.length() - 4).trim(), ""); + GameUtils.addGame(game); + GameUtils.launch(requireActivity(), game); + }); + }).start(); + } + @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java index bfe33a2b..4ac73661 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java @@ -8,7 +8,7 @@ import com.panda3ds.pandroid.R; import com.panda3ds.pandroid.app.PreferenceActivity; import com.panda3ds.pandroid.app.base.BasePreferenceFragment; import com.panda3ds.pandroid.app.preferences.AppearancePreferences; -import com.panda3ds.pandroid.app.preferences.DeveloperPreferences; +import com.panda3ds.pandroid.app.preferences.AdvancedPreferences; import com.panda3ds.pandroid.app.preferences.InputPreferences; public class SettingsFragment extends BasePreferenceFragment { @@ -17,6 +17,6 @@ public class SettingsFragment extends BasePreferenceFragment { setPreferencesFromResource(R.xml.start_preferences, rootKey); setItemClick("input", (item) -> PreferenceActivity.launch(requireContext(), InputPreferences.class)); setItemClick("appearance", (item)-> PreferenceActivity.launch(requireContext(), AppearancePreferences.class)); - setItemClick("developer", (item)-> PreferenceActivity.launch(requireContext(), DeveloperPreferences.class)); + setItemClick("advanced", (item)-> PreferenceActivity.launch(requireContext(), AdvancedPreferences.class)); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/DeveloperPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AdvancedPreferences.java similarity index 90% rename from src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/DeveloperPreferences.java rename to src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AdvancedPreferences.java index f131f0a0..fea8aef0 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/DeveloperPreferences.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AdvancedPreferences.java @@ -13,11 +13,11 @@ import com.panda3ds.pandroid.app.base.BasePreferenceFragment; import com.panda3ds.pandroid.app.services.LoggerService; import com.panda3ds.pandroid.data.config.GlobalConfig; -public class DeveloperPreferences extends BasePreferenceFragment { +public class AdvancedPreferences extends BasePreferenceFragment { @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { - setPreferencesFromResource(R.xml.developer_preferences, rootKey); - setActivityTitle(R.string.developer_options); + setPreferencesFromResource(R.xml.advanced_preferences, rootKey); + setActivityTitle(R.string.advanced_options); setItemClick("performanceMonitor", pref -> GlobalConfig.set(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY, ((SwitchPreference) pref).isChecked())); setItemClick("shaderJit", pref -> GlobalConfig.set(GlobalConfig.KEY_SHADER_JIT, ((SwitchPreference) pref).isChecked())); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java index dea4e261..04c89d9a 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java @@ -15,7 +15,7 @@ public class AppearancePreferences extends BasePreferenceFragment { public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.appearance_preference, rootKey); - ((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.appearance); + setActivityTitle(R.string.appearance); SingleSelectionPreferences themePreference = findPreference("theme"); themePreference.setSelectedItem(GlobalConfig.get(GlobalConfig.KEY_APP_THEME)); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java index e59adfbe..f643c88f 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java @@ -37,7 +37,9 @@ public class ControllerMapperPreferences extends Fragment { currentProfile = ControllerProfileManager.get(getArguments().getString("profile")).clone(); - ((BaseActivity) requireActivity()).getSupportActionBar().hide(); + if (((BaseActivity)requireActivity()).getSupportActionBar() != null) { + ((BaseActivity) requireActivity()).getSupportActionBar().hide(); + } mapper = view.findViewById(R.id.mapper); mapper.initialize(this::onLocationChanged, currentProfile); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java index b4d148b9..10fa10f9 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputMapPreferences.java @@ -27,7 +27,7 @@ public class InputMapPreferences extends BasePreferenceFragment implements Activ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.input_map_preferences, rootKey); - ((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.controller_mapping); + setActivityTitle(R.string.controller_mapping); for (KeyName key : KeyName.values()) { if (key == KeyName.NULL) { diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java index 512a3725..5acf2593 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/game/GameMetadata.java @@ -43,6 +43,10 @@ public class GameMetadata { return romPath; } + public String getRealPath() { + return GameUtils.resolvePath(romPath); + } + public String getId() { return id; } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java index c72a516a..28276920 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java @@ -26,4 +26,6 @@ public class Constants { public static final String PREF_GAME_UTILS = "app.GameUtils"; public static final String PREF_INPUT_MAP = "app.InputMap"; public static final String PREF_SCREEN_CONTROLLER_PROFILES = "app.input.ScreenControllerManager"; + public static final String RESOURCE_FOLDER_ELF = "ELF"; // Folder for caching ELF files + public static final String RESOURCE_FOLDER_LUA_SCRIPTS = "Lua Scripts"; } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java index 1746f1c9..2920c7c6 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/FileUtils.java @@ -39,7 +39,7 @@ public class FileUtils { return parseFile(path).getName(); } - public static String getResourcesPath(){ + public static String getResourcesPath() { File file = new File(getPrivatePath(), "config/resources"); if (!file.exists()) { file.mkdirs(); @@ -48,6 +48,13 @@ public class FileUtils { return file.getAbsolutePath(); } + public static String getResourcePath(String name) { + File file = new File(getResourcesPath(), name); + file.mkdirs(); + + return file.getAbsolutePath(); + } + public static String getPrivatePath() { File file = getContext().getFilesDir(); if (!file.exists()) { @@ -66,11 +73,33 @@ public class FileUtils { return file.getAbsolutePath(); } + public static String parseNativeMode(String mode) { + mode = mode.toLowerCase(); + switch (mode) { + case "r": + case "rb": + return "r"; + case "r+": + case "r+b": + case "rb+": + return "rw"; + case "w+": + return "rwt"; + case "w": + case "wb": + return "wt"; + case "wa": + return "wa"; + } + + throw new IllegalArgumentException("Invalid file mode: "+mode); + } + public static boolean exists(String path) { return parseFile(path).exists(); } - public static void rename(String path, String newName){ + public static void rename(String path, String newName) { parseFile(path).renameTo(newName); } @@ -206,7 +235,7 @@ public class FileUtils { } } - public static void updateFile(String path){ + public static void updateFile(String path) { DocumentFile file = parseFile(path); Uri uri = file.getUri(); @@ -232,15 +261,53 @@ public class FileUtils { return parseFile(path).lastModified(); } - public static String[] listFiles(String path){ + public static String[] listFiles(String path) { DocumentFile folder = parseFile(path); DocumentFile[] files = folder.listFiles(); String[] result = new String[files.length]; - for (int i = 0; i < result.length; i++){ + for (int i = 0; i < result.length; i++) { result[i] = files[i].getName(); } return result; } + + public static Uri obtainUri(String path) { + return parseFile(path).getUri(); + } + + public static String extension(String uri) { + String name = getName(uri); + if (!name.contains(".")) { + return name.toLowerCase(); + } + String[] parts = name.split("\\."); + + return parts[parts.length-1].toLowerCase(); + } + + public static boolean copyFile(String source, String path, String name) { + try { + String fullPath = path + "/" + name; + if (!FileUtils.exists(fullPath)) { + FileUtils.delete(fullPath); + } + FileUtils.createFile(path, name); + InputStream in = getInputStream(source); + OutputStream out = getOutputStream(fullPath); + byte[] buffer = new byte[1024 * 128]; //128 KB + int length; + while ((length = in.read(buffer)) != -1) { + out.write(buffer, 0, length); + } + out.flush(); + out.close(); + in.close(); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "ERROR ON COPY FILE", e); + return false; + } + return true; + } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java index b763f7b2..f050af0a 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/GameUtils.java @@ -31,7 +31,7 @@ public class GameUtils { public static GameMetadata findByRomPath(String romPath) { for (GameMetadata game : data.games) { - if (Objects.equals(romPath, game.getRomPath())) { + if (Objects.equals(romPath, game.getRealPath())) { return game; } } @@ -40,7 +40,13 @@ public class GameUtils { public static void launch(Context context, GameMetadata game) { currentGame = game; - String path = FileUtils.obtainRealPath(game.getRomPath()); + String path = game.getRealPath(); + if (path.contains("://")) { + String[] parts = Uri.decode(game.getRomPath()).split("/"); + String name = parts[parts.length - 1]; + path = "game://internal/" + name; + } + context.startActivity(new Intent(context, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path)); } @@ -58,6 +64,22 @@ public class GameUtils { writeChanges(); } + public static String resolvePath(String path) { + String lower = path.toLowerCase(); + if (!lower.contains("://")) { + return path; + } + + Uri uri = Uri.parse(path); + switch (uri.getScheme().toLowerCase()) { + case "elf": { + return FileUtils.getResourcePath(Constants.RESOURCE_FOLDER_ELF)+"/"+uri.getAuthority(); + } + } + + return path; + } + public static ArrayList getGames() { return new ArrayList<>(data.games); } 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 c39b36b3..76dc5e7d 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 @@ -2,20 +2,24 @@ package com.panda3ds.pandroid.view; import static android.opengl.GLES32.*; +import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.opengl.GLSurfaceView; +import android.os.Handler; import android.util.Log; +import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.data.SMDH; import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.data.game.GameMetadata; import com.panda3ds.pandroid.utils.Constants; import com.panda3ds.pandroid.utils.GameUtils; import com.panda3ds.pandroid.utils.PerformanceMonitor; import com.panda3ds.pandroid.view.renderer.ConsoleRenderer; import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout; import com.panda3ds.pandroid.view.renderer.layout.DefaultScreenLayout; -import com.panda3ds.pandroid.data.SMDH; -import com.panda3ds.pandroid.data.game.GameMetadata; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -25,9 +29,11 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer private int screenWidth, screenHeight; private int screenTexture; public int screenFbo; + private final Context context; - PandaGlRenderer(String romPath) { + PandaGlRenderer(Context context, String romPath) { super(); + this.context = context; this.romPath = romPath; screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels; @@ -40,8 +46,8 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer if (screenTexture != 0) { glDeleteTextures(1, new int[] {screenTexture}, 0); } - - if (screenFbo != 0) { + + if (screenFbo != 0) { glDeleteFramebuffers(1, new int[] {screenFbo}, 0); } @@ -84,7 +90,29 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer AlberDriver.Initialize(); AlberDriver.setShaderJitEnabled(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT)); - AlberDriver.LoadRom(romPath); + + // If loading the ROM failed, display an error message and early exit + if (!AlberDriver.LoadRom(romPath)) { + // Get a handler that can be used to post to the main thread + Handler mainHandler = new Handler(context.getMainLooper()); + + Runnable runnable = new Runnable() { + @Override + public void run() { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle("Failed to load ROM") + .setMessage("Make sure it's a valid 3DS ROM and that storage permissions are configured properly.") + .setPositiveButton("OK", null) + .setCancelable(false) + .show(); + } + }; + mainHandler.post(runnable); + + GameMetadata game = GameUtils.getCurrentGame(); + GameUtils.removeGame(game); + return; + } // Load the SMDH byte[] smdhData = AlberDriver.GetSmdh(); @@ -93,12 +121,12 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer } else { SMDH smdh = new SMDH(smdhData); Log.i(Constants.LOG_TAG, "Loaded rom SDMH"); - Log.i(Constants.LOG_TAG, String.format("Are you playing '%s' published by '%s'", smdh.getTitle(), smdh.getPublisher())); + Log.i(Constants.LOG_TAG, String.format("You are playing '%s' published by '%s'", smdh.getTitle(), smdh.getPublisher())); GameMetadata game = GameUtils.getCurrentGame(); GameUtils.removeGame(game); GameUtils.addGame(GameMetadata.applySMDH(game, smdh)); } - + PerformanceMonitor.initialize(getBackendName()); } @@ -150,4 +178,4 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer public String getBackendName() { return "OpenGL"; } -} \ No newline at end of file +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java index c813294c..e3023fcb 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaGlSurfaceView.java @@ -21,7 +21,7 @@ public class PandaGlSurfaceView extends GLSurfaceView implements TouchScreenNode if (Debug.isDebuggerConnected()) { setDebugFlags(DEBUG_LOG_GL_CALLS); } - renderer = new PandaGlRenderer(romPath); + renderer = new PandaGlRenderer(getContext(), romPath); setRenderer(renderer); } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java index 1d497656..caabae6b 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/BasicTextEditor.java @@ -93,7 +93,7 @@ public class BasicTextEditor extends AppCompatEditText { super.scrollTo(scrollX, scrollY); } - public void adjustScroll(){ + public void adjustScroll() { setScroll(getScrollX(), getScrollY()); } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java index e3e5128a..accb0326 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/code/syntax/PatternUtils.java @@ -3,16 +3,17 @@ package com.panda3ds.pandroid.view.code.syntax; import java.util.regex.Pattern; class PatternUtils { - public static Pattern buildGenericKeywords(String... keywords){ - StringBuilder builder = new StringBuilder(); - builder.append("\\b("); - for (int i = 0; i < keywords.length; i++){ - builder.append(keywords[i]); - if (i+1 != keywords.length){ - builder.append("|"); - } - } - builder.append(")\\b"); - return Pattern.compile(builder.toString()); - } + public static Pattern buildGenericKeywords(String... keywords) { + StringBuilder builder = new StringBuilder(); + builder.append("\\b("); + for (int i = 0; i < keywords.length; i++) { + builder.append(keywords[i]); + if (i + 1 != keywords.length) { + builder.append("|"); + } + } + + builder.append(")\\b"); + return Pattern.compile(builder.toString()); + } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/utils/PerformanceView.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/utils/PerformanceView.java index e4d7be15..7688b44e 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/utils/PerformanceView.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/utils/PerformanceView.java @@ -34,7 +34,7 @@ public class PerformanceView extends AppCompatTextView { setShadowLayer(padding,0,0,Color.BLACK); } - public void refresh(){ + public void refresh() { running = isShown(); if (!running) { return; diff --git a/src/pandroid/app/src/main/res/drawable/color_surface.xml b/src/pandroid/app/src/main/res/drawable/color_surface.xml new file mode 100644 index 00000000..b8655b87 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/color_surface.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/medium_card_background.xml b/src/pandroid/app/src/main/res/drawable/medium_card_background.xml new file mode 100644 index 00000000..805248ea --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/medium_card_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/rounded_selectable_item_background.xml b/src/pandroid/app/src/main/res/drawable/rounded_selectable_item_background.xml new file mode 100644 index 00000000..16b39ced --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/rounded_selectable_item_background.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/pandroid/app/src/main/res/drawable/simple_card_background.xml b/src/pandroid/app/src/main/res/drawable/simple_card_background.xml index 88845ce4..f07f345c 100644 --- a/src/pandroid/app/src/main/res/drawable/simple_card_background.xml +++ b/src/pandroid/app/src/main/res/drawable/simple_card_background.xml @@ -1,9 +1,5 @@ - - - - - - - - \ No newline at end of file + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/switch_thumb.xml b/src/pandroid/app/src/main/res/drawable/switch_thumb.xml new file mode 100644 index 00000000..02f1ab02 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/switch_thumb.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/switch_track.xml b/src/pandroid/app/src/main/res/drawable/switch_track.xml new file mode 100644 index 00000000..b665789c --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/switch_track.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout-land/activity_main.xml b/src/pandroid/app/src/main/res/layout-land/activity_main.xml index fa4cfbca..9741809d 100644 --- a/src/pandroid/app/src/main/res/layout-land/activity_main.xml +++ b/src/pandroid/app/src/main/res/layout-land/activity_main.xml @@ -5,7 +5,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".app.MainActivity"> + tools:context=".app.MainActivity" + android:background="?colorSurface"> + style="@style/ThemedNavigationBottom" + android:background="@drawable/color_surface"/> \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/activity_input_map.xml b/src/pandroid/app/src/main/res/layout/activity_input_map.xml index cbacc64e..79249e62 100644 --- a/src/pandroid/app/src/main/res/layout/activity_input_map.xml +++ b/src/pandroid/app/src/main/res/layout/activity_input_map.xml @@ -4,7 +4,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:gravity="center"> + android:gravity="center" + android:background="?colorSurface"> + tools:context=".app.MainActivity" + android:background="?colorSurface"> + style="@style/ThemedNavigationBottom" + android:background="@drawable/color_surface"/> - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/layout/activity_preference.xml b/src/pandroid/app/src/main/res/layout/activity_preference.xml index 54b3d364..401c3d86 100644 --- a/src/pandroid/app/src/main/res/layout/activity_preference.xml +++ b/src/pandroid/app/src/main/res/layout/activity_preference.xml @@ -3,7 +3,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" - android:orientation="vertical"> + android:orientation="vertical" + android:background="?colorSurface"> + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml b/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml index 69a9d0a4..b0484879 100644 --- a/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml +++ b/src/pandroid/app/src/main/res/layout/dialog_lua_scripts.xml @@ -1,9 +1,9 @@ - @@ -12,7 +12,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - - - - + - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml b/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml index fa81a503..8019c3c3 100644 --- a/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml +++ b/src/pandroid/app/src/main/res/layout/fragment_game_drawer.xml @@ -1,105 +1,94 @@ - - - + + android:layout_height="match_parent" + android:orientation="vertical"> - - - - - - - + + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:minHeight="190dp" + android:background="?colorSurfaceVariant"> - + + + + + + + android:layout_marginBottom="20dp"> - + - + - + - + + + + + + android:orientation="vertical" + android:gravity="start" + android:layout_marginTop="195dp"> - + app:menu="@menu/game_drawer" + android:background="?colorSurface" + android:theme="@style/Widget.App.NavigationView" + app:subheaderTextAppearance="@style/TextAppearanceGameDrawerSubTitle"/> - + - - - - - - - - - - - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/layout/fragment_games.xml b/src/pandroid/app/src/main/res/layout/fragment_games.xml index ee69fdc8..33033834 100644 --- a/src/pandroid/app/src/main/res/layout/fragment_games.xml +++ b/src/pandroid/app/src/main/res/layout/fragment_games.xml @@ -1,5 +1,6 @@ @@ -12,14 +13,15 @@ + android:elevation="5dp" + app:backgroundTint="?colorPrimary"/> - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/layout/fragment_search.xml b/src/pandroid/app/src/main/res/layout/fragment_search.xml index 5872a404..e165ff32 100644 --- a/src/pandroid/app/src/main/res/layout/fragment_search.xml +++ b/src/pandroid/app/src/main/res/layout/fragment_search.xml @@ -11,14 +11,14 @@ android:layout_height="64dp" android:paddingHorizontal="20dp"> - @@ -35,15 +35,15 @@ + android:layout_height="match_parent"> + android:layout_height="match_parent" + android:paddingStart="15dp" + android:paddingEnd="15dp"/> - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/layout/holder_game.xml b/src/pandroid/app/src/main/res/layout/holder_game.xml index b2d46d1b..f7e077c2 100644 --- a/src/pandroid/app/src/main/res/layout/holder_game.xml +++ b/src/pandroid/app/src/main/res/layout/holder_game.xml @@ -4,13 +4,16 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" + android:foreground="@drawable/rounded_selectable_item_background" + android:focusable="true" android:orientation="vertical" android:padding="15dp"> - + app:cardCornerRadius="10dp" + app:strokeWidth="0dp"> - + + android:gravity="center" + android:layout_marginTop="10dp"/> - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/layout/holder_lua_script.xml b/src/pandroid/app/src/main/res/layout/holder_lua_script.xml index a1865c3f..c4410011 100644 --- a/src/pandroid/app/src/main/res/layout/holder_lua_script.xml +++ b/src/pandroid/app/src/main/res/layout/holder_lua_script.xml @@ -1,43 +1,51 @@ - + app:cardBackgroundColor="?attr/colorSurfaceVariant" + android:layout_gravity="center" + android:layout_margin="10dp" + app:strokeWidth="0px" + app:cardCornerRadius="8dp"> - - - + android:orientation="vertical" + android:gravity="center" + android:padding="10dp"> - + + - + + + - - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/layout/preference_simple_about.xml b/src/pandroid/app/src/main/res/layout/preference_simple_about.xml index 9364de36..cf6370b9 100644 --- a/src/pandroid/app/src/main/res/layout/preference_simple_about.xml +++ b/src/pandroid/app/src/main/res/layout/preference_simple_about.xml @@ -2,6 +2,7 @@ @@ -26,4 +27,4 @@ android:alpha="0.5" android:textColor="?colorOnSurface"/> - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/menu/game_drawer.xml b/src/pandroid/app/src/main/res/menu/game_drawer.xml new file mode 100644 index 00000000..72574d7b --- /dev/null +++ b/src/pandroid/app/src/main/res/menu/game_drawer.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/src/pandroid/app/src/main/res/menu/game_drawer_actions.xml b/src/pandroid/app/src/main/res/menu/game_drawer_actions.xml deleted file mode 100644 index 9fd3264a..00000000 --- a/src/pandroid/app/src/main/res/menu/game_drawer_actions.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/menu/game_drawer_others.xml b/src/pandroid/app/src/main/res/menu/game_drawer_others.xml deleted file mode 100644 index b6dd4897..00000000 --- a/src/pandroid/app/src/main/res/menu/game_drawer_others.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml index 1198d66b..96a47941 100644 --- a/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml +++ b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml @@ -45,8 +45,8 @@ Abrir arquivo Criar novo Executando \"%s\" ... - Opções de desenvolvedor - Depuração, mostrar fps, etc. + Opções avançada. + Depuração, mostrar fps, etc. Monitor de desempenho Mostrar um overlay com fps, memoria, etc. Depuração @@ -54,4 +54,6 @@ Shader Jit Usar recompilador de shaders. Gráficos + Carregando + Rotacionar diff --git a/src/pandroid/app/src/main/res/values-v27/themes.xml b/src/pandroid/app/src/main/res/values-v27/themes.xml new file mode 100644 index 00000000..8e960864 --- /dev/null +++ b/src/pandroid/app/src/main/res/values-v27/themes.xml @@ -0,0 +1,16 @@ + + + + + + + + - diff --git a/src/pandroid/app/src/main/res/xml/developer_preferences.xml b/src/pandroid/app/src/main/res/xml/advanced_preferences.xml similarity index 94% rename from src/pandroid/app/src/main/res/xml/developer_preferences.xml rename to src/pandroid/app/src/main/res/xml/advanced_preferences.xml index 96ce8906..ce77a6e4 100644 --- a/src/pandroid/app/src/main/res/xml/developer_preferences.xml +++ b/src/pandroid/app/src/main/res/xml/advanced_preferences.xml @@ -12,6 +12,7 @@ android:key="loggerService" app:iconSpaceReserved="false" app:title="@string/pref_logger_service_title" + app:defaultValue="true" android:summary="@string/pref_logger_service_summary"/> + diff --git a/src/pandroid/app/src/main/res/xml/start_preferences.xml b/src/pandroid/app/src/main/res/xml/start_preferences.xml index 5eeb1954..f41e83a2 100644 --- a/src/pandroid/app/src/main/res/xml/start_preferences.xml +++ b/src/pandroid/app/src/main/res/xml/start_preferences.xml @@ -24,10 +24,10 @@ app:layout="@layout/preference_start_item"/> \ No newline at end of file diff --git a/third_party/miniaudio b/third_party/miniaudio new file mode 160000 index 00000000..4a5b74be --- /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 00000000..01db7cdd --- /dev/null +++ b/third_party/teakra @@ -0,0 +1 @@ +Subproject commit 01db7cdd00aabcce559a8dddce8798dabb71949b