diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml index 7625e18d..137577c1 100644 --- a/.github/workflows/Android_Build.yml +++ b/.github/workflows/Android_Build.yml @@ -6,15 +6,19 @@ on: - master pull_request: -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - jobs: x64: runs-on: ubuntu-latest + strategy: + matrix: + build_type: + - release + steps: + - name: Set BUILD_TYPE variable + run: echo "BUILD_TYPE=${{ matrix.build_type }}" >> $GITHUB_ENV + - uses: actions/checkout@v2 - name: Fetch submodules run: git submodule update --init --recursive @@ -29,7 +33,7 @@ jobs: - name: Setup Java uses: actions/setup-java@v3 with: - distribution: 'zulu' # See 'Supported distributions' for available options + distribution: 'zulu' java-version: '17' - name: Configure CMake @@ -37,23 +41,36 @@ jobs: - name: Build run: | + # Apply patch for GLES compatibility git apply ./.github/gles.patch - cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + # Build the project with CMake + cmake --build ${{github.workspace}}/build --config ${{ env.BUILD_TYPE }} + # Move the generated library to the appropriate location mv ./build/libAlber.so ./src/pandroid/app/src/main/jniLibs/x86_64/ + # Build the Android app with Gradle cd src/pandroid - ./gradlew assembleDebug + ./gradlew assemble${{ env.BUILD_TYPE }} cd ../.. - - name: Upload executable + - name: Upload artifacts uses: actions/upload-artifact@v2 with: - name: Android APK (x86-64) - path: './src/pandroid/app/build/outputs/apk/debug/app-debug.apk' + name: Android APKs (x86-64) + path: | + ./src/pandroid/app/build/outputs/apk/${{ env.BUILD_TYPE }}/app-${{ env.BUILD_TYPE }}.apk arm64: runs-on: ubuntu-latest + strategy: + matrix: + build_type: + - release + steps: + - name: Set BUILD_TYPE variable + run: echo "BUILD_TYPE=${{ matrix.build_type }}" >> $GITHUB_ENV + - uses: actions/checkout@v2 - name: Fetch submodules run: git submodule update --init --recursive @@ -68,7 +85,7 @@ jobs: - name: Setup Java uses: actions/setup-java@v3 with: - distribution: 'zulu' # See 'Supported distributions' for available options + distribution: 'zulu' java-version: '17' - name: Configure CMake @@ -76,16 +93,21 @@ jobs: - name: Build run: | + # Apply patch for GLES compatibility git apply ./.github/gles.patch - cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + # Build the project with CMake + cmake --build ${{github.workspace}}/build --config ${{ env.BUILD_TYPE }} + # Move the generated library to the appropriate location mv ./build/libAlber.so ./src/pandroid/app/src/main/jniLibs/arm64-v8a/ + # Build the Android app with Gradle cd src/pandroid - ./gradlew assembleDebug + ./gradlew assemble${{ env.BUILD_TYPE }} + ls -R app/build/outputs cd ../.. - - name: Upload executable + - name: Upload artifacts uses: actions/upload-artifact@v2 with: - name: Android APK (arm64) - path: './src/pandroid/app/build/outputs/apk/debug/app-debug.apk' - + name: Android APKs (arm64) + path: | + ./src/pandroid/app/build/outputs/apk/${{ env.BUILD_TYPE }}/app-${{ env.BUILD_TYPE }}.apk diff --git a/.gitmodules b/.gitmodules index 3735d0cb..f1e8f469 100644 --- a/.gitmodules +++ b/.gitmodules @@ -49,3 +49,9 @@ [submodule "third_party/oaknut"] path = third_party/oaknut url = https://github.com/merryhime/oaknut +[submodule "third_party/luv"] + path = third_party/luv + url = https://github.com/luvit/luv +[submodule "third_party/libuv"] + path = third_party/libuv + url = https://github.com/libuv/libuv diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d5df370..81572703 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,17 +188,19 @@ set(FS_SOURCE_FILES src/core/fs/archive_self_ncch.cpp src/core/fs/archive_save_d src/core/fs/ivfc.cpp src/core/fs/archive_user_save_data.cpp src/core/fs/archive_system_save_data.cpp ) -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) +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(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) # Frontend source files if(NOT ANDROID) if(ENABLE_QT_GUI) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp - src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp + src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp - include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp + include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) @@ -244,7 +246,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.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/PICA/shader_gen.hpp + include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/PICA/shader_gen.hpp ) cmrc_add_resource_library( @@ -264,6 +266,16 @@ set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp third_party/xxhash/xxhash.c ) +if(ENABLE_LUAJIT AND NOT ANDROID) + # Build luv and libuv for Lua TCP server usage if we're not on Android + include_directories(third_party/luv/src) + include_directories(third_party/luv/deps/lua-compat-5.3/c-api) + include_directories(third_party/libuv/include) + set(THIRD_PARTY_SOURCE_FILES ${THIRD_PARTY_SOURCE_FILES} third_party/luv/src/luv.c) + set(LIBUV_BUILD_SHARED OFF) + + add_subdirectory(third_party/libuv) +endif() if(ENABLE_QT_GUI) include_directories(third_party/duckstation) @@ -431,6 +443,11 @@ endif() if(ENABLE_LUAJIT) target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_LUA=1") target_link_libraries(Alber PRIVATE libluajit) + + # If we're not on Android, link libuv too + if (NOT ANDROID) + target_link_libraries(Alber PRIVATE uv_a) + endif() endif() if(ENABLE_OPENGL) diff --git a/include/applets/applet.hpp b/include/applets/applet.hpp index 79fba1cb..48f20b03 100644 --- a/include/applets/applet.hpp +++ b/include/applets/applet.hpp @@ -1,6 +1,9 @@ #pragma once +#include + #include "helpers.hpp" +#include "kernel/kernel_types.hpp" #include "memory.hpp" #include "result/result.hpp" @@ -65,10 +68,11 @@ namespace Applets { }; struct Parameter { - u32 senderID; - u32 destID; - u32 signal; - std::vector data; + u32 senderID; // ID of the parameter sender + u32 destID; // ID of the app to receive parameter + u32 signal; // Signal type (eg request) + u32 object; // Some applets will also respond with shared memory handles for transferring data between the sender and called + std::vector data; // Misc data }; class AppletBase { @@ -80,7 +84,7 @@ namespace Applets { virtual const char* name() = 0; // Called by APT::StartLibraryApplet and similar - virtual Result::HorizonResult start() = 0; + virtual Result::HorizonResult start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) = 0; // Transfer parameters from application -> applet virtual Result::HorizonResult receiveParameter(const Parameter& parameter) = 0; virtual void reset() = 0; diff --git a/include/applets/applet_manager.hpp b/include/applets/applet_manager.hpp index e75e1268..d8cfff12 100644 --- a/include/applets/applet_manager.hpp +++ b/include/applets/applet_manager.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include "applets/error_applet.hpp" #include "applets/mii_selector.hpp" #include "applets/software_keyboard.hpp" #include "helpers.hpp" @@ -11,6 +12,7 @@ namespace Applets { class AppletManager { MiiSelectorApplet miiSelector; SoftwareKeyboardApplet swkbd; + ErrorApplet error; std::optional nextParameter = std::nullopt; public: diff --git a/include/applets/error_applet.hpp b/include/applets/error_applet.hpp new file mode 100644 index 00000000..4dcc319d --- /dev/null +++ b/include/applets/error_applet.hpp @@ -0,0 +1,15 @@ +#include + +#include "applets/applet.hpp" + +namespace Applets { + class ErrorApplet final : public AppletBase { + public: + virtual const char* name() override { return "Error/EULA Agreement"; } + virtual Result::HorizonResult start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) override; + virtual Result::HorizonResult receiveParameter(const Applets::Parameter& parameter) override; + virtual void reset() override; + + ErrorApplet(Memory& memory, std::optional& nextParam) : AppletBase(memory, nextParam) {} + }; +} // namespace Applets \ No newline at end of file diff --git a/include/applets/mii_selector.hpp b/include/applets/mii_selector.hpp index c90d4b59..36f9fe79 100644 --- a/include/applets/mii_selector.hpp +++ b/include/applets/mii_selector.hpp @@ -1,13 +1,83 @@ +#include + #include "applets/applet.hpp" +#include "swap.hpp" namespace Applets { + struct MiiConfig { + u8 enableCancelButton; + u8 enableGuestMii; + u8 showOnTopScreen; + std::array pad1; + std::array title; + std::array pad2; + u8 showGuestMiis; + std::array pad3; + u32 initiallySelectedIndex; + std::array guestMiiWhitelist; + std::array userMiiWhitelist; + std::array pad4; + u32 magicValue; + }; + static_assert(sizeof(MiiConfig) == 0x104, "Mii config size is wrong"); + + // Some members of this struct are not properly aligned so we need pragma pack +#pragma pack(push, 1) + struct MiiData { + u8 version; + u8 miiOptions; + u8 miiPos; + u8 consoleID; + + u64_be systemID; + u32_be miiID; + std::array creatorMAC; + u16 padding; + + u16_be miiDetails; + std::array miiName; + u8 height; + u8 width; + + u8 faceStyle; + u8 faceDetails; + u8 hairStyle; + u8 hairDetails; + u32_be eyeDetails; + u32_be eyebrowDetails; + u16_be noseDetails; + u16_be mouthDetails; + u16_be moustacheDetails; + u16_be beardDetails; + u16_be glassesDetails; + u16_be moleDetails; + + std::array authorName; + }; +#pragma pack(pop) + static_assert(sizeof(MiiData) == 0x5C, "MiiData structure has incorrect size"); + + struct MiiResult { + u32_be returnCode; + u32_be isGuestMiiSelected; + u32_be selectedGuestMiiIndex; + MiiData selectedMiiData; + u16_be unknown1; + u16_be miiChecksum; + std::array guestMiiName; + }; + static_assert(sizeof(MiiResult) == 0x84, "MiiResult structure has incorrect size"); + class MiiSelectorApplet final : public AppletBase { public: virtual const char* name() override { return "Mii Selector"; } - virtual Result::HorizonResult start() override; + virtual Result::HorizonResult start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) override; virtual Result::HorizonResult receiveParameter(const Applets::Parameter& parameter) override; virtual void reset() override; + MiiResult output; + MiiConfig config; + MiiResult getDefaultMii(); MiiSelectorApplet(Memory& memory, std::optional& nextParam) : AppletBase(memory, nextParam) {} }; } // namespace Applets \ No newline at end of file diff --git a/include/applets/software_keyboard.hpp b/include/applets/software_keyboard.hpp index f4179012..f753566d 100644 --- a/include/applets/software_keyboard.hpp +++ b/include/applets/software_keyboard.hpp @@ -1,13 +1,162 @@ +#include + #include "applets/applet.hpp" +#include "swap.hpp" namespace Applets { + // Software keyboard definitions adapted from libctru/Citra + // Keyboard input filtering flags. Allows the caller to specify what input is explicitly not allowed + namespace SoftwareKeyboardFilter { + enum Filter : u32 { + Digits = 1, // Disallow the use of more than a certain number of digits (0 or more) + At = 1 << 1, // Disallow the use of the @ sign. + Percent = 1 << 2, // Disallow the use of the % sign. + Backslash = 1 << 3, // Disallow the use of the \ sign. + Profanity = 1 << 4, // Disallow profanity using Nintendo's profanity filter. + Callback = 1 << 5, // Use a callback in order to check the input. + }; + } // namespace SoftwareKeyboardFilter + + // Keyboard features. + namespace SoftwareKeyboardFeature { + enum Feature { + Parental = 1, // Parental PIN mode. + DarkenTopScreen = 1 << 1, // Darken the top screen when the keyboard is shown. + PredictiveInput = 1 << 2, // Enable predictive input (necessary for Kanji input in JPN systems). + Multiline = 1 << 3, // Enable multiline input. + FixedWidth = 1 << 4, // Enable fixed-width mode. + AllowHome = 1 << 5, // Allow the usage of the HOME button. + AllowReset = 1 << 6, // Allow the usage of a software-reset combination. + AllowPower = 1 << 7, // Allow the usage of the POWER button. + DefaultQWERTY = 1 << 9, // Default to the QWERTY page when the keyboard is shown. + }; + } // namespace SoftwareKeyboardFeature + class SoftwareKeyboardApplet final : public AppletBase { public: + static constexpr int MAX_BUTTON = 3; // Maximum number of buttons that can be in the keyboard. + static constexpr int MAX_BUTTON_TEXT_LEN = 16; // Maximum button text length, in UTF-16 code units. + static constexpr int MAX_HINT_TEXT_LEN = 64; // Maximum hint text length, in UTF-16 code units. + static constexpr int MAX_CALLBACK_MSG_LEN = 256; // Maximum filter callback error message length, in UTF-16 code units. + + // Keyboard types + enum class SoftwareKeyboardType : u32 { + Normal, // Normal keyboard with several pages (QWERTY/accents/symbol/mobile) + QWERTY, // QWERTY keyboard only. + NumPad, // Number pad. + Western, // On JPN systems, a text keyboard without Japanese input capabilities, otherwise same as SWKBD_TYPE_NORMAL. + }; + + // Keyboard dialog buttons. + enum class SoftwareKeyboardButtonConfig : u32 { + SingleButton, // Ok button + DualButton, // Cancel | Ok buttons + TripleButton, // Cancel | I Forgot | Ok buttons + NoButton, // No button (returned by swkbdInputText in special cases) + }; + + // Accepted input types. + enum class SoftwareKeyboardValidInput : u32 { + Anything, // All inputs are accepted. + NotEmpty, // Empty inputs are not accepted. + NotEmptyNotBlank, // Empty or blank inputs (consisting solely of whitespace) are not accepted. + NotBlank, // Blank inputs (consisting solely of whitespace) are not accepted, but empty inputs are. + FixedLen, // The input must have a fixed length (specified by maxTextLength in swkbdInit) + }; + + // Keyboard password modes. + enum class SoftwareKeyboardPasswordMode : u32 { + None, // Characters are not concealed. + Hide, // Characters are concealed immediately. + HideDelay, // Characters are concealed a second after they've been typed. + }; + + // Keyboard filter callback return values. + enum class SoftwareKeyboardCallbackResult : u32 { + OK, // Specifies that the input is valid. + Close, // Displays an error message, then closes the keyboard. + Continue, // Displays an error message and continues displaying the keyboard. + }; + + // Keyboard return values. + enum class SoftwareKeyboardResult : s32 { + None = -1, // Dummy/unused. + InvalidInput = -2, // Invalid parameters to swkbd. + OutOfMem = -3, // Out of memory. + + D0Click = 0, // The button was clicked in 1-button dialogs. + D1Click0, // The left button was clicked in 2-button dialogs. + D1Click1, // The right button was clicked in 2-button dialogs. + D2Click0, // The left button was clicked in 3-button dialogs. + D2Click1, // The middle button was clicked in 3-button dialogs. + D2Click2, // The right button was clicked in 3-button dialogs. + + HomePressed = 10, // The HOME button was pressed. + ResetPressed, // The soft-reset key combination was pressed. + PowerPressed, // The POWER button was pressed. + + ParentalOK = 20, // The parental PIN was verified successfully. + ParentalFail, // The parental PIN was incorrect. + + BannedInput = 30, // The filter callback returned SoftwareKeyboardCallback::CLOSE. + }; + + struct SoftwareKeyboardConfig { + enum_le type; + enum_le numButtonsM1; + enum_le validInput; + enum_le passwordMode; + s32_le isParentalScreen; + s32_le darkenTopScreen; + u32_le filterFlags; + u32_le saveStateFlags; + u16_le maxTextLength; + u16_le dictWordCount; + u16_le maxDigits; + std::array, MAX_BUTTON> buttonText; + std::array numpadKeys; + std::array hintText; // Text to display when asking the user for input + bool predictiveInput; + bool multiline; + bool fixedWidth; + bool allowHome; + bool allowReset; + bool allowPower; + bool unknown; + bool defaultQwerty; + std::array buttonSubmitsText; + u16_le language; + + u32_le initialTextOffset; // Offset of the default text in the output SharedMemory + u32_le dictOffset; + u32_le initialStatusOffset; + u32_le initialLearningOffset; + u32_le sharedMemorySize; // Size of the SharedMemory + u32_le version; + + enum_le returnCode; + + u32_le statusOffset; + u32_le learningOffset; + + u32_le textOffset; // Offset in the SharedMemory where the output text starts + u16_le textLength; // Length in characters of the output text + + enum_le callbackResult; + std::array callbackMessage; + bool skipAtCheck; + std::array pad; + }; + static_assert(sizeof(SoftwareKeyboardConfig) == 0x400, "Software keyboard config size is wrong"); + virtual const char* name() override { return "Software Keyboard"; } - virtual Result::HorizonResult start() override; + virtual Result::HorizonResult start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) override; virtual Result::HorizonResult receiveParameter(const Applets::Parameter& parameter) override; virtual void reset() override; SoftwareKeyboardApplet(Memory& memory, std::optional& nextParam) : AppletBase(memory, nextParam) {} + void closeKeyboard(u32 appID); + + SoftwareKeyboardConfig config; }; } // namespace Applets \ No newline at end of file diff --git a/include/cheats.hpp b/include/cheats.hpp index 2be25827..b90c080b 100644 --- a/include/cheats.hpp +++ b/include/cheats.hpp @@ -24,6 +24,7 @@ class Cheats { Cheats(Memory& mem, HIDService& hid); u32 addCheat(const Cheat& cheat); + u32 addCheat(const u8* data, size_t size); void removeCheat(u32 id); void enableCheat(u32 id); void disableCheat(u32 id); @@ -32,6 +33,7 @@ class Cheats { void clear(); bool haveCheats() const { return cheatsLoaded; } + static constexpr u32 badCheatHandle = 0xFFFFFFFF; private: ActionReplay ar; // An ActionReplay cheat machine for executing CTRPF codes diff --git a/include/cpu_dynarmic.hpp b/include/cpu_dynarmic.hpp index 8f1e277b..048bc62a 100644 --- a/include/cpu_dynarmic.hpp +++ b/include/cpu_dynarmic.hpp @@ -9,15 +9,18 @@ #include "helpers.hpp" #include "kernel.hpp" #include "memory.hpp" +#include "scheduler.hpp" +class Emulator; class CPU; class MyEnvironment final : public Dynarmic::A32::UserCallbacks { -public: - u64 ticksLeft = 0; - u64 totalTicks = 0; - Memory& mem; - Kernel& kernel; + public: + u64 ticksLeft = 0; + u64 totalTicks = 0; + Memory& mem; + Kernel& kernel; + Scheduler& scheduler; u64 getCyclesForInstruction(bool isThumb, u32 instruction); @@ -76,54 +79,56 @@ public: std::terminate(); } - void CallSVC(u32 swi) override { - kernel.serviceSVC(swi); - } + void CallSVC(u32 swi) override { + kernel.serviceSVC(swi); + } - void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override { - switch (exception) { - case Dynarmic::A32::Exception::UnpredictableInstruction: - Helpers::panic("Unpredictable instruction at pc = %08X", pc); - break; + void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override { + switch (exception) { + case Dynarmic::A32::Exception::UnpredictableInstruction: + Helpers::panic("Unpredictable instruction at pc = %08X", pc); + break; - default: Helpers::panic("Fired exception oops"); - } - } + default: Helpers::panic("Fired exception oops"); + } + } - void AddTicks(u64 ticks) override { - totalTicks += ticks; + void AddTicks(u64 ticks) override { + scheduler.currentTimestamp += ticks; - if (ticks > ticksLeft) { - ticksLeft = 0; - return; - } - ticksLeft -= ticks; - } + if (ticks > ticksLeft) { + ticksLeft = 0; + return; + } + ticksLeft -= ticks; + } - u64 GetTicksRemaining() override { - return ticksLeft; - } + u64 GetTicksRemaining() override { + return ticksLeft; + } - u64 GetTicksForCode(bool isThumb, u32 vaddr, u32 instruction) override { - return getCyclesForInstruction(isThumb, instruction); - } + u64 GetTicksForCode(bool isThumb, u32 vaddr, u32 instruction) override { + return getCyclesForInstruction(isThumb, instruction); + } - MyEnvironment(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} + MyEnvironment(Memory& mem, Kernel& kernel, Scheduler& scheduler) : mem(mem), kernel(kernel), scheduler(scheduler) {} }; class CPU { - std::unique_ptr jit; - std::shared_ptr cp15; + std::unique_ptr jit; + std::shared_ptr cp15; - // Make exclusive monitor with only 1 CPU core - Dynarmic::ExclusiveMonitor exclusiveMonitor{1}; - MyEnvironment env; - Memory& mem; + // Make exclusive monitor with only 1 CPU core + Dynarmic::ExclusiveMonitor exclusiveMonitor{1}; + MyEnvironment env; + Memory& mem; + Scheduler& scheduler; + Emulator& emu; -public: - static constexpr u64 ticksPerSec = 268111856; + public: + static constexpr u64 ticksPerSec = Scheduler::arm11Clock; - CPU(Memory& mem, Kernel& kernel); + CPU(Memory& mem, Kernel& kernel, Emulator& emu); void reset(); void setReg(int index, u32 value) { @@ -162,29 +167,20 @@ public: } u64 getTicks() { - return env.totalTicks; + return scheduler.currentTimestamp; } // Get reference to tick count. Memory needs access to this u64& getTicksRef() { - return env.totalTicks; + return scheduler.currentTimestamp; } - void clearCache() { jit->ClearCache(); } - - void runFrame() { - env.ticksLeft = ticksPerSec / 60; - execute: - const auto exitReason = jit->Run(); - - if (static_cast(exitReason) != 0) [[unlikely]] { - // Cache invalidation needs to exit the JIT so it returns a CacheInvalidation HaltReason. In our case, we just go back to executing - // The goto might be terrible but it does guarantee that this does not recursively call run and crash, instead getting optimized to a jump - if (Dynarmic::Has(exitReason, Dynarmic::HaltReason::CacheInvalidation)) { - goto execute; - } else { - Helpers::panic("Exit reason: %d\nPC: %08X", static_cast(exitReason), getReg(15)); - } - } + Scheduler& getScheduler() { + return scheduler; } + + void addTicks(u64 ticks) { env.AddTicks(ticks); } + + void clearCache() { jit->ClearCache(); } + void runFrame(); }; \ No newline at end of file diff --git a/include/emulator.hpp b/include/emulator.hpp index f4537425..d3377f6c 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -15,6 +15,7 @@ #include "io_file.hpp" #include "lua_manager.hpp" #include "memory.hpp" +#include "scheduler.hpp" #ifdef PANDA3DS_ENABLE_HTTP_SERVER #include "http_server.hpp" @@ -42,6 +43,7 @@ class Emulator { Kernel kernel; Crypto::AESEngine aesEngine; Cheats cheats; + Scheduler scheduler; // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state @@ -85,6 +87,8 @@ class Emulator { // change ROMs. If Reload is selected, the emulator will reload its selected ROM. This is useful for eg a "reset" button that keeps the current // ROM and just resets the emu enum class ReloadOption { NoReload, Reload }; + // Used in CPU::runFrame + bool frameDone = false; Emulator(); ~Emulator(); @@ -94,6 +98,8 @@ class Emulator { void reset(ReloadOption reload); void run(void* frontend = nullptr); void runFrame(); + // Poll the scheduler for events + void pollScheduler(); void resume(); // Resume the emulator void pause(); // Pause the emulator @@ -121,6 +127,7 @@ class Emulator { Cheats& getCheats() { return cheats; } ServiceManager& getServiceManager() { return kernel.getServiceManager(); } LuaManager& getLua() { return lua; } + Scheduler& getScheduler() { return scheduler; } RendererType getRendererType() const { return config.rendererType; } Renderer* getRenderer() { return gpu.getRenderer(); } @@ -128,6 +135,8 @@ class Emulator { std::filesystem::path getConfigPath(); std::filesystem::path getAndroidAppPath(); + // Get the root path for the emulator's app data + std::filesystem::path getAppDataRoot(); std::span getSMDH(); }; diff --git a/include/fs/archive_sdmc.hpp b/include/fs/archive_sdmc.hpp index 4aa80eab..f63731c4 100644 --- a/include/fs/archive_sdmc.hpp +++ b/include/fs/archive_sdmc.hpp @@ -5,8 +5,10 @@ using Result::HorizonResult; class SDMCArchive : public ArchiveBase { -public: - SDMCArchive(Memory& mem) : ArchiveBase(mem) {} + bool isWriteOnly = false; // There's 2 variants of the SDMC archive: Regular one (Read/Write) and write-only + + public: + SDMCArchive(Memory& mem, bool writeOnly = false) : ArchiveBase(mem), isWriteOnly(writeOnly) {} u64 getFreeBytes() override { return 1_GB; } std::string name() override { return "SDMC"; } diff --git a/include/kernel/handles.hpp b/include/kernel/handles.hpp index b038049f..fe746b65 100644 --- a/include/kernel/handles.hpp +++ b/include/kernel/handles.hpp @@ -53,6 +53,7 @@ namespace KernelHandles { GSPSharedMemHandle = MaxServiceHandle + 1, // Handle for the GSP shared memory FontSharedMemHandle, CSNDSharedMemHandle, + APTCaptureSharedMemHandle, // Shared memory for display capture info, HIDSharedMemHandle, MinSharedMemHandle = GSPSharedMemHandle, diff --git a/include/kernel/kernel.hpp b/include/kernel/kernel.hpp index 3f09bf12..e78a588a 100644 --- a/include/kernel/kernel.hpp +++ b/include/kernel/kernel.hpp @@ -70,6 +70,7 @@ public: 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 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); @@ -94,7 +95,7 @@ public: void releaseMutex(Mutex* moo); void cancelTimer(Timer* timer); void signalTimer(Handle timerHandle, Timer* timer); - void updateTimer(Handle timerHandle, Timer* timer); + u64 getWakeupTick(s64 ns); // Wake up the thread with the highest priority out of all threads in the waitlist // Returns the index of the woken up thread @@ -137,6 +138,7 @@ public: void duplicateHandle(); void exitThread(); void mapMemoryBlock(); + void unmapMemoryBlock(); void queryMemory(); void getCurrentProcessorNumber(); void getProcessID(); diff --git a/include/kernel/kernel_types.hpp b/include/kernel/kernel_types.hpp index 53c60774..01af4bd9 100644 --- a/include/kernel/kernel_types.hpp +++ b/include/kernel/kernel_types.hpp @@ -83,56 +83,53 @@ struct Port { }; struct Session { - Handle portHandle; // The port this session is subscribed to - Session(Handle portHandle) : portHandle(portHandle) {} + Handle portHandle; // The port this session is subscribed to + Session(Handle portHandle) : portHandle(portHandle) {} }; enum class ThreadStatus { - Running, // Currently running - Ready, // Ready to run - WaitArbiter, // Waiting on an address arbiter - WaitSleep, // Waiting due to a SleepThread SVC - WaitSync1, // Waiting for the single object in the wait list to be ready - WaitSyncAny, // Wait for one object of the many that might be in the wait list to be ready - WaitSyncAll, // Waiting for ALL sync objects in its wait list to be ready - WaitIPC, // Waiting for the reply from an IPC request - Dormant, // Created but not yet made ready - Dead // Run to completion, or forcefully terminated + Running, // Currently running + Ready, // Ready to run + WaitArbiter, // Waiting on an address arbiter + WaitSleep, // Waiting due to a SleepThread SVC + WaitSync1, // Waiting for the single object in the wait list to be ready + WaitSyncAny, // Wait for one object of the many that might be in the wait list to be ready + WaitSyncAll, // Waiting for ALL sync objects in its wait list to be ready + WaitIPC, // Waiting for the reply from an IPC request + Dormant, // Created but not yet made ready + Dead // Run to completion, or forcefully terminated }; struct Thread { - u32 initialSP; // Initial r13 value - u32 entrypoint; // Initial r15 value - u32 priority; - u32 arg; - ProcessorID processorID; - ThreadStatus status; - Handle handle; // OS handle for this thread - int index; // Index of the thread. 0 for the first thread, 1 for the second, and so on + u32 initialSP; // Initial r13 value + u32 entrypoint; // Initial r15 value + u32 priority; + u32 arg; + ProcessorID processorID; + ThreadStatus status; + Handle handle; // OS handle for this thread + int index; // Index of the thread. 0 for the first thread, 1 for the second, and so on - // The waiting address for threads that are waiting on an AddressArbiter - u32 waitingAddress; + // The waiting address for threads that are waiting on an AddressArbiter + u32 waitingAddress; - // The nanoseconds until a thread wakes up from being asleep or from timing out while waiting on an arbiter - u64 waitingNanoseconds; - // The tick this thread went to sleep on - u64 sleepTick; - // For WaitSynchronization(N): A vector of objects this thread is waiting for - std::vector waitList; - // For WaitSynchronizationN: Shows whether the object should wait for all objects in the wait list or just one - bool waitAll; - // For WaitSynchronizationN: The "out" pointer - u32 outPointer; + // For WaitSynchronization(N): A vector of objects this thread is waiting for + std::vector waitList; + // For WaitSynchronizationN: Shows whether the object should wait for all objects in the wait list or just one + bool waitAll; + // For WaitSynchronizationN: The "out" pointer + u32 outPointer; + u64 wakeupTick; - // Thread context used for switching between threads - std::array gprs; - std::array fprs; // Stored as u32 because dynarmic does it - u32 cpsr; - u32 fpscr; - u32 tlsBase; // Base pointer for thread-local storage + // Thread context used for switching between threads + std::array gprs; + std::array fprs; // Stored as u32 because dynarmic does it + u32 cpsr; + u32 fpscr; + u32 tlsBase; // Base pointer for thread-local storage - // A list of threads waiting for this thread to terminate. Yes, threads are sync objects too. - u64 threadsWaitingForTermination; + // A list of threads waiting for this thread to terminate. Yes, threads are sync objects too. + u64 threadsWaitingForTermination; }; static const char* kernelObjectTypeToString(KernelObjectType t) { @@ -177,13 +174,12 @@ struct Timer { u64 waitlist; // Refer to the getWaitlist function below for documentation ResetType resetType = ResetType::OneShot; - u64 startTick; // CPU tick the timer started - u64 currentDelay; // Number of ns until the timer fires next time + u64 fireTick; // CPU tick the timer will be fired u64 interval; // Number of ns until the timer fires for the second and future times bool fired; // Has this timer been signalled? bool running; // Is this timer running or stopped? - Timer(ResetType type) : resetType(type), startTick(0), currentDelay(0), interval(0), waitlist(0), fired(false), running(false) {} + Timer(ResetType type) : resetType(type), fireTick(0), interval(0), waitlist(0), fired(false), running(false) {} }; struct MemoryBlock { diff --git a/include/logger.hpp b/include/logger.hpp index 82d90410..e021a685 100644 --- a/include/logger.hpp +++ b/include/logger.hpp @@ -2,6 +2,10 @@ #include #include +#ifdef __ANDROID__ +#include +#endif + namespace Log { // Our logger class template @@ -12,7 +16,11 @@ namespace Log { std::va_list args; va_start(args, fmt); +#ifdef __ANDROID__ + __android_log_vprint(ANDROID_LOG_DEFAULT, "Panda3DS", fmt, args); +#else std::vprintf(fmt, args); +#endif va_end(args); } }; @@ -81,4 +89,4 @@ namespace Log { #else #define MAKE_LOG_FUNCTION(functionName, logger) MAKE_LOG_FUNCTION_USER(functionName, logger) #endif -} +} \ No newline at end of file diff --git a/include/memory.hpp b/include/memory.hpp index 3ddd00cc..640ae5f0 100644 --- a/include/memory.hpp +++ b/include/memory.hpp @@ -112,11 +112,12 @@ class Memory { // This tracks our OS' memory allocations std::vector memoryInfo; - std::array sharedMemBlocks = { + std::array sharedMemBlocks = { SharedMemoryBlock(0, 0, KernelHandles::FontSharedMemHandle), // Shared memory for the system font (size is 0 because we read the size from the cmrc filesystem SharedMemoryBlock(0, 0x1000, KernelHandles::GSPSharedMemHandle), // GSP shared memory SharedMemoryBlock(0, 0x1000, KernelHandles::HIDSharedMemHandle), // HID shared memory SharedMemoryBlock(0, 0x3000, KernelHandles::CSNDSharedMemHandle), // CSND shared memory + SharedMemoryBlock(0, 0xE7000, KernelHandles::APTCaptureSharedMemHandle), // APT Capture Buffer memory }; public: diff --git a/include/panda_qt/cheats_window.hpp b/include/panda_qt/cheats_window.hpp new file mode 100644 index 00000000..c82b2bd8 --- /dev/null +++ b/include/panda_qt/cheats_window.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + +#include "emulator.hpp" + +class QListWidget; + +class CheatsWindow final : public QWidget { + Q_OBJECT + + public: + CheatsWindow(Emulator* emu, const std::filesystem::path& path, QWidget* parent = nullptr); + ~CheatsWindow() = default; + + private: + void addEntry(); + void removeClicked(); + + QListWidget* cheatList; + std::filesystem::path cheatPath; + Emulator* emu; +}; diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 7dfb91b7..c2db9ac1 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -12,16 +13,38 @@ #include "emulator.hpp" #include "panda_qt/about_window.hpp" #include "panda_qt/config_window.hpp" +#include "panda_qt/cheats_window.hpp" #include "panda_qt/screen.hpp" #include "panda_qt/text_editor.hpp" #include "services/hid.hpp" +struct CheatMessage { + u32 handle; + std::vector cheat; + std::function callback; +}; + class MainWindow : public QMainWindow { Q_OBJECT private: // Types of messages we might send from the GUI thread to the emulator thread - enum class MessageType { LoadROM, Reset, Pause, Resume, TogglePause, DumpRomFS, PressKey, ReleaseKey, LoadLuaScript }; + enum class MessageType { + LoadROM, + Reset, + Pause, + Resume, + TogglePause, + DumpRomFS, + PressKey, + ReleaseKey, + SetCirclePadX, + SetCirclePadY, + LoadLuaScript, + EditCheat, + PressTouchscreen, + ReleaseTouchscreen, + }; // Tagged union representing our message queue messages struct EmulatorMessage { @@ -36,9 +59,22 @@ class MainWindow : public QMainWindow { u32 key; } key; + struct { + s16 value; + } circlepad; + struct { std::string* str; } string; + + struct { + CheatMessage* c; + } cheat; + + struct { + u16 x; + u16 y; + } touchscreen; }; }; @@ -54,6 +90,7 @@ class MainWindow : public QMainWindow { ScreenWidget screen; AboutWindow* aboutWindow; ConfigWindow* configWindow; + CheatsWindow* cheatsEditor; TextEditorWindow* luaEditor; QMenuBar* menuBar = nullptr; @@ -63,6 +100,7 @@ class MainWindow : public QMainWindow { void selectROM(); void dumpRomFS(); void openLuaEditor(); + void openCheatsEditor(); void showAboutMenu(); void sendMessage(const EmulatorMessage& message); void dispatchMessage(const EmulatorMessage& message); @@ -77,5 +115,9 @@ class MainWindow : public QMainWindow { void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void loadLuaScript(const std::string& code); + void editCheat(u32 handle, const std::vector& cheat, const std::function& callback); }; \ No newline at end of file diff --git a/include/scheduler.hpp b/include/scheduler.hpp new file mode 100644 index 00000000..5645f47d --- /dev/null +++ b/include/scheduler.hpp @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include + +#include "helpers.hpp" +#include "logger.hpp" + +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) + TotalNumberOfEvents // How many event types do we have in total? + }; + static constexpr usize totalNumberOfEvents = static_cast(EventType::TotalNumberOfEvents); + static constexpr u64 arm11Clock = 268111856; + + template + using EventMap = boost::container::flat_multimap, boost::container::static_vector, size>>; + + EventMap events; + u64 currentTimestamp = 0; + u64 nextTimestamp = 0; + + // Set nextTimestamp to the timestamp of the next event + void updateNextTimestamp() { nextTimestamp = events.cbegin()->first; } + + void addEvent(EventType type, u64 timestamp) { + events.emplace(timestamp, type); + updateNextTimestamp(); + } + + void removeEvent(EventType type) { + for (auto it = events.begin(); it != events.end(); it++) { + // Find first event of type "type" and remove it. + // Our scheduler shouldn't have duplicate events, so it's safe to exit when an event is found + if (it->second == type) { + events.erase(it); + updateNextTimestamp(); + break; + } + } + }; + + void reset() { + currentTimestamp = 0; + + // Clear any pending events + events.clear(); + addEvent(Scheduler::EventType::VBlank, arm11Clock / 60); + + // Add a dummy event to always keep the scheduler non-empty + addEvent(EventType::Panic, std::numeric_limits::max()); + } + + private: + static constexpr u64 MAX_VALUE_TO_MULTIPLY = std::numeric_limits::max() / arm11Clock; + + public: + // Function for converting time units to cycles for various kernel functions + // Thank you Citra + static constexpr s64 nsToCycles(float ns) { return s64(arm11Clock * (0.000000001f) * ns); } + static constexpr s64 nsToCycles(int ns) { return arm11Clock * s64(ns) / 1000000000; } + + static constexpr s64 nsToCycles(s64 ns) { + if (ns / 1000000000 > static_cast(MAX_VALUE_TO_MULTIPLY)) { + return std::numeric_limits::max(); + } + + if (ns > static_cast(MAX_VALUE_TO_MULTIPLY)) { + return arm11Clock * (ns / 1000000000); + } + + return (arm11Clock * ns) / 1000000000; + } + + static constexpr s64 nsToCycles(u64 ns) { + if (ns / 1000000000 > MAX_VALUE_TO_MULTIPLY) { + return std::numeric_limits::max(); + } + + if (ns > MAX_VALUE_TO_MULTIPLY) { + return arm11Clock * (s64(ns) / 1000000000); + } + + return (arm11Clock * s64(ns)) / 1000000000; + } +}; \ No newline at end of file diff --git a/include/services/boss.hpp b/include/services/boss.hpp index 57452e2d..769184e5 100644 --- a/include/services/boss.hpp +++ b/include/services/boss.hpp @@ -14,6 +14,7 @@ class BOSSService { void cancelTask(u32 messagePointer); void initializeSession(u32 messagePointer); void getErrorCode(u32 messagePointer); + void getNewArrivalFlag(u32 messagePointer); void getNsDataIdList(u32 messagePointer, u32 commandWord); void getOptoutFlag(u32 messagePointer); void getStorageEntryInfo(u32 messagePointer); // Unknown what this is, name taken from Citra diff --git a/include/services/cam.hpp b/include/services/cam.hpp index 611a3b6d..60ede3b9 100644 --- a/include/services/cam.hpp +++ b/include/services/cam.hpp @@ -12,22 +12,45 @@ class Kernel; class CAMService { + using Event = std::optional; + + struct Port { + Event bufferErrorInterruptEvent = std::nullopt; + Event receiveEvent = std::nullopt; + u16 transferBytes; + + void reset() { + bufferErrorInterruptEvent = std::nullopt; + receiveEvent = std::nullopt; + transferBytes = 256; + } + }; + Handle handle = KernelHandles::CAM; Memory& mem; Kernel& kernel; MAKE_LOG_FUNCTION(log, camLogger) - using Event = std::optional; - static constexpr size_t portCount = 4; // PORT_NONE, PORT_CAM1, PORT_CAM2, PORT_BOTH - std::array bufferErrorInterruptEvents; + static constexpr size_t portCount = 2; + std::array ports; // Service commands void driverInitialize(u32 messagePointer); + void driverFinalize(u32 messagePointer); + void getMaxBytes(u32 messagePointer); void getMaxLines(u32 messagePointer); void getBufferErrorInterruptEvent(u32 messagePointer); + void getSuitableY2RCoefficients(u32 messagePointer); + void getTransferBytes(u32 messagePointer); void setContrast(u32 messagePointer); void setFrameRate(u32 messagePointer); + void setReceiving(u32 messagePointer); + void setSize(u32 messagePointer); + void setTransferBytes(u32 messagePointer); void setTransferLines(u32 messagePointer); + void setTrimming(u32 messagePointer); + void setTrimmingParamsCenter(u32 messagePointer); + void startCapture(u32 messagePointer); public: CAMService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} diff --git a/include/services/fs.hpp b/include/services/fs.hpp index 2fe3ba5d..4a613121 100644 --- a/include/services/fs.hpp +++ b/include/services/fs.hpp @@ -26,6 +26,7 @@ class FSService { SelfNCCHArchive selfNcch; SaveDataArchive saveData; SDMCArchive sdmc; + SDMCArchive sdmcWriteOnly; NCCHArchive ncch; // UserSaveData archives @@ -61,6 +62,7 @@ class FSService { void getFreeBytes(u32 messagePointer); void getFormatInfo(u32 messagePointer); void getPriority(u32 messagePointer); + void getSdmcArchiveResource(u32 messagePointer); void getThisSaveDataSecureValue(u32 messagePointer); void theGameboyVCFunction(u32 messagePointer); void initialize(u32 messagePointer); @@ -81,9 +83,9 @@ class FSService { public: FSService(Memory& mem, Kernel& kernel, const EmulatorConfig& config) - : mem(mem), saveData(mem), sharedExtSaveData_nand(mem, "../SharedFiles/NAND", true), extSaveData_sdmc(mem, "SDMC"), sdmc(mem), selfNcch(mem), - ncch(mem), userSaveData1(mem, ArchiveID::UserSaveData1), userSaveData2(mem, ArchiveID::UserSaveData2), kernel(kernel), config(config), - systemSaveData(mem) {} + : mem(mem), saveData(mem), sharedExtSaveData_nand(mem, "../SharedFiles/NAND", true), extSaveData_sdmc(mem, "SDMC"), sdmc(mem), + sdmcWriteOnly(mem, true), selfNcch(mem), ncch(mem), userSaveData1(mem, ArchiveID::UserSaveData1), + userSaveData2(mem, ArchiveID::UserSaveData2), kernel(kernel), config(config), systemSaveData(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/gsp_gpu.hpp b/include/services/gsp_gpu.hpp index 68e24580..1e48190c 100644 --- a/include/services/gsp_gpu.hpp +++ b/include/services/gsp_gpu.hpp @@ -60,12 +60,22 @@ class GPUService { }; static_assert(sizeof(FramebufferUpdate) == 64, "GSP::GPU::FramebufferUpdate has the wrong size"); + // Used for saving and restoring GPU state via ImportDisplayCaptureInfo + struct CaptureInfo { + u32 leftFramebuffer; // Left framebuffer VA + u32 rightFramebuffer; // Right framebuffer VA (Top screen only) + u32 format; + u32 stride; + }; + static_assert(sizeof(CaptureInfo) == 16, "GSP::GPU::CaptureInfo has the wrong size"); + // Service commands void acquireRight(u32 messagePointer); void flushDataCache(u32 messagePointer); void importDisplayCaptureInfo(u32 messagePointer); void registerInterruptRelayQueue(u32 messagePointer); void releaseRight(u32 messagePointer); + void restoreVramSysArea(u32 messagePointer); void saveVramSysArea(u32 messagePointer); void setAxiConfigQoSMode(u32 messagePointer); void setBufferSwap(u32 messagePointer); @@ -86,6 +96,15 @@ class GPUService { void setBufferSwapImpl(u32 screen_id, const FramebufferInfo& info); + // Get the framebuffer info in shared memory for a given screen + FramebufferUpdate* getFramebufferInfo(int screen) { + // TODO: Offset depends on GSP thread being triggered + return reinterpret_cast(&sharedMem[0x200 + screen * sizeof(FramebufferUpdate)]); + } + + FramebufferUpdate* getTopFramebufferInfo() { return getFramebufferInfo(0); } + FramebufferUpdate* getBottomFramebufferInfo() { return getFramebufferInfo(1); } + public: GPUService(Memory& mem, GPU& gpu, Kernel& kernel, u32& currentPID) : mem(mem), gpu(gpu), kernel(kernel), currentPID(currentPID) {} diff --git a/include/services/ptm.hpp b/include/services/ptm.hpp index ae845725..f752839b 100644 --- a/include/services/ptm.hpp +++ b/include/services/ptm.hpp @@ -17,6 +17,7 @@ class PTMService { void getAdapterState(u32 messagePointer); void getBatteryChargeState(u32 messagePointer); void getBatteryLevel(u32 messagePointer); + void getPedometerState(u32 messagePointer); void getStepHistory(u32 messagePointer); void getStepHistoryAll(u32 messagePointer); void getTotalStepCount(u32 messagePointer); diff --git a/include/services/y2r.hpp b/include/services/y2r.hpp index 0a1cae2f..a08c41a2 100644 --- a/include/services/y2r.hpp +++ b/include/services/y2r.hpp @@ -98,6 +98,7 @@ class Y2RService { void setSendingY(u32 messagePointer); void setSendingU(u32 messagePointer); void setSendingV(u32 messagePointer); + void setSendingYUV(u32 messagePointer); void setSpacialDithering(u32 messagePointer); void setStandardCoeff(u32 messagePointer); void setTemporalDithering(u32 messagePointer); diff --git a/src/core/CPU/cpu_dynarmic.cpp b/src/core/CPU/cpu_dynarmic.cpp index 29ca49d1..da5270b4 100644 --- a/src/core/CPU/cpu_dynarmic.cpp +++ b/src/core/CPU/cpu_dynarmic.cpp @@ -1,32 +1,59 @@ #ifdef CPU_DYNARMIC #include "cpu_dynarmic.hpp" + #include "arm_defs.hpp" +#include "emulator.hpp" -CPU::CPU(Memory& mem, Kernel& kernel) : mem(mem), env(mem, kernel) { - cp15 = std::make_shared(); +CPU::CPU(Memory& mem, Kernel& kernel, Emulator& emu) : mem(mem), emu(emu), scheduler(emu.getScheduler()), env(mem, kernel, emu.getScheduler()) { + cp15 = std::make_shared(); - Dynarmic::A32::UserConfig config; - config.arch_version = Dynarmic::A32::ArchVersion::v6K; - config.callbacks = &env; - config.coprocessors[15] = cp15; - config.define_unpredictable_behaviour = true; - config.global_monitor = &exclusiveMonitor; - config.processor_id = 0; - - jit = std::make_unique(config); + Dynarmic::A32::UserConfig config; + config.arch_version = Dynarmic::A32::ArchVersion::v6K; + config.callbacks = &env; + config.coprocessors[15] = cp15; + config.define_unpredictable_behaviour = true; + config.global_monitor = &exclusiveMonitor; + config.processor_id = 0; + + jit = std::make_unique(config); } void CPU::reset() { - setCPSR(CPSR::UserMode); - setFPSCR(FPSCR::MainThreadDefault); - env.totalTicks = 0; + 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 - jit->Reset(); - jit->ClearCache(); - jit->Regs().fill(0); - jit->ExtRegs().fill(0); + cp15->reset(); + cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage + jit->Reset(); + jit->ClearCache(); + jit->Regs().fill(0); + jit->ExtRegs().fill(0); } -#endif // CPU_DYNARMIC \ No newline at end of file +void CPU::runFrame() { + emu.frameDone = false; + + while (!emu.frameDone) { + // Run CPU until the next scheduler event + env.ticksLeft = scheduler.nextTimestamp - scheduler.currentTimestamp; + + execute: + const auto exitReason = jit->Run(); + + // Handle any scheduler events that need handling. + emu.pollScheduler(); + + if (static_cast(exitReason) != 0) [[unlikely]] { + // Cache invalidation needs to exit the JIT so it returns a CacheInvalidation HaltReason. In our case, we just go back to executing + // The goto might be terrible but it does guarantee that this does not recursively call run and crash, instead getting optimized to a jump + if (Dynarmic::Has(exitReason, Dynarmic::HaltReason::CacheInvalidation)) { + goto execute; + } else { + Helpers::panic("Exit reason: %d\nPC: %08X", static_cast(exitReason), getReg(15)); + } + } + } +} + +#endif // CPU_DYNARMIC \ No newline at end of file diff --git a/src/core/action_replay.cpp b/src/core/action_replay.cpp index e8467425..1ef494a2 100644 --- a/src/core/action_replay.cpp +++ b/src/core/action_replay.cpp @@ -139,6 +139,65 @@ void ActionReplay::executeDType(const Cheat& cheat, u32 instruction) { switch (instruction) { case 0xD3000000: offset1 = cheat[pc++]; break; case 0xD3000001: offset2 = cheat[pc++]; break; + + case 0xD6000000: + write32(*activeOffset + cheat[pc++], u32(*activeData)); + *activeOffset += 4; + break; + + case 0xD6000001: + write32(*activeOffset + cheat[pc++], u32(data1)); + *activeOffset += 4; + break; + + case 0xD6000002: + write32(*activeOffset + cheat[pc++], u32(data2)); + *activeOffset += 4; + break; + + case 0xD7000000: + write16(*activeOffset + cheat[pc++], u16(*activeData)); + *activeOffset += 2; + break; + + case 0xD7000001: + write16(*activeOffset + cheat[pc++], u16(data1)); + *activeOffset += 2; + break; + + case 0xD7000002: + write16(*activeOffset + cheat[pc++], u16(data2)); + *activeOffset += 2; + break; + + case 0xD8000000: + write8(*activeOffset + cheat[pc++], u8(*activeData)); + *activeOffset += 1; + break; + + case 0xD8000001: + write8(*activeOffset + cheat[pc++], u8(data1)); + *activeOffset += 1; + break; + + case 0xD8000002: + write8(*activeOffset + cheat[pc++], u8(data2)); + *activeOffset += 1; + break; + + + case 0xD9000000: *activeData = read32(cheat[pc++] + *activeOffset); break; + case 0xD9000001: data1 = read32(cheat[pc++] + *activeOffset); break; + case 0xD9000002: data2 = read32(cheat[pc++] + *activeOffset); break; + + case 0xDA000000: *activeData = read16(cheat[pc++] + *activeOffset); break; + case 0xDA000001: data1 = read16(cheat[pc++] + *activeOffset); break; + case 0xDA000002: data2 = read16(cheat[pc++] + *activeOffset); break; + + case 0xDB000000: *activeData = read8(cheat[pc++] + *activeOffset); break; + case 0xDB000001: data1 = read8(cheat[pc++] + *activeOffset); break; + case 0xDB000002: data2 = read8(cheat[pc++] + *activeOffset); break; + case 0xDC000000: *activeOffset += cheat[pc++]; break; // DD000000 XXXXXXXX - if KEYPAD has value XXXXXXXX execute next block diff --git a/src/core/applets/applet_manager.cpp b/src/core/applets/applet_manager.cpp index c2791777..cdb19319 100644 --- a/src/core/applets/applet_manager.cpp +++ b/src/core/applets/applet_manager.cpp @@ -4,13 +4,14 @@ using namespace Applets; -AppletManager::AppletManager(Memory& mem) : miiSelector(mem, nextParameter), swkbd(mem, nextParameter) {} +AppletManager::AppletManager(Memory& mem) : miiSelector(mem, nextParameter), swkbd(mem, nextParameter), error(mem, nextParameter) {} void AppletManager::reset() { nextParameter = std::nullopt; miiSelector.reset(); swkbd.reset(); + error.reset(); } AppletBase* AppletManager::getApplet(u32 id) { @@ -21,6 +22,9 @@ AppletBase* AppletManager::getApplet(u32 id) { case AppletIDs::SoftwareKeyboard: case AppletIDs::SoftwareKeyboard2: return &swkbd; + case AppletIDs::ErrDisp: + case AppletIDs::ErrDisp2: return &error; + default: return nullptr; } } diff --git a/src/core/applets/error_applet.cpp b/src/core/applets/error_applet.cpp new file mode 100644 index 00000000..5acbcbba --- /dev/null +++ b/src/core/applets/error_applet.cpp @@ -0,0 +1,32 @@ +#include "applets/error_applet.hpp" +#include "kernel/handles.hpp" + +using namespace Applets; + +void ErrorApplet::reset() {} + +Result::HorizonResult ErrorApplet::start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) { + Applets::Parameter param = Applets::Parameter{ + .senderID = appID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::WakeupByExit), + .object = 0, + .data = parameters, // TODO: Figure out how the data format for this applet + }; + + nextParameter = param; + return Result::Success; +} + +Result::HorizonResult ErrorApplet::receiveParameter(const Applets::Parameter& parameter) { + Applets::Parameter param = Applets::Parameter{ + .senderID = parameter.destID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::Response), + .object = KernelHandles::APTCaptureSharedMemHandle, + .data = {}, + }; + + nextParameter = param; + return Result::Success; +} \ No newline at end of file diff --git a/src/core/applets/mii_selector.cpp b/src/core/applets/mii_selector.cpp index f392d846..ab6455fc 100644 --- a/src/core/applets/mii_selector.cpp +++ b/src/core/applets/mii_selector.cpp @@ -1,11 +1,86 @@ #include "applets/mii_selector.hpp" +#include +#include + +#include "kernel/handles.hpp" + using namespace Applets; void MiiSelectorApplet::reset() {} -Result::HorizonResult MiiSelectorApplet::start() { return Result::Success; } +Result::HorizonResult MiiSelectorApplet::start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) { + // Get mii configuration from the application + std::memcpy(&config, ¶meters[0], sizeof(config)); + + Applets::Parameter param = Applets::Parameter{ + .senderID = appID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::WakeupByExit), + .object = 0, + }; + + // Thanks to Citra devs as always for the default mii data and other applet help + output = getDefaultMii(); + output.returnCode = 0; // Success + output.selectedGuestMiiIndex = std::numeric_limits::max(); + output.miiChecksum = boost::crc<16, 0x1021, 0, 0, false, false>(&output.selectedMiiData, sizeof(MiiData) + sizeof(output.unknown1)); + + // Copy output into the response parameter + param.data.resize(sizeof(output)); + std::memcpy(¶m.data[0], &output, sizeof(output)); + + nextParameter = param; + return Result::Success; +} Result::HorizonResult MiiSelectorApplet::receiveParameter(const Applets::Parameter& parameter) { - Helpers::warn("Mii Selector: Unimplemented ReceiveParameter"); + Applets::Parameter param = Applets::Parameter{ + .senderID = parameter.destID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::Response), + .object = KernelHandles::APTCaptureSharedMemHandle, + .data = {}, + }; + + nextParameter = param; return Result::Success; -} \ No newline at end of file +} + +MiiResult MiiSelectorApplet::getDefaultMii() { + // This data was obtained from Citra + MiiData miiData; + miiData.version = 0x03; + miiData.miiOptions = 0x00; + miiData.miiPos = 0x10; + miiData.consoleID = 0x30; + miiData.systemID = 0xD285B6B300C8850A; + miiData.miiID = 0x98391EE4; + miiData.creatorMAC = {0x40, 0xF4, 0x07, 0xB7, 0x37, 0x10}; + miiData.padding = 0x0000; + miiData.miiDetails = 0xA600; + miiData.miiName = {'P', 'a', 'n', 'd', 'a', '3', 'D', 'S', 0x0, 0x0}; + miiData.height = 0x40; + miiData.width = 0x40; + miiData.faceStyle = 0x00; + miiData.faceDetails = 0x00; + miiData.hairStyle = 0x21; + miiData.hairDetails = 0x01; + miiData.eyeDetails = 0x02684418; + miiData.eyebrowDetails = 0x26344614; + miiData.noseDetails = 0x8112; + miiData.mouthDetails = 0x1768; + miiData.moustacheDetails = 0x0D00; + miiData.beardDetails = 0x0029; + miiData.glassesDetails = 0x0052; + miiData.moleDetails = 0x4850; + miiData.authorName = {u'B', u'O', u'N', u'K', u'E', u'R'}; + + MiiResult result; + result.returnCode = 0x0; + result.isGuestMiiSelected = 0x0; + result.selectedGuestMiiIndex = std::numeric_limits::max(); + result.selectedMiiData = miiData; + result.guestMiiName.fill(0x0); + + return result; +} diff --git a/src/core/applets/software_keyboard.cpp b/src/core/applets/software_keyboard.cpp index be5a9e21..4a91b790 100644 --- a/src/core/applets/software_keyboard.cpp +++ b/src/core/applets/software_keyboard.cpp @@ -1,20 +1,93 @@ #include "applets/software_keyboard.hpp" +#include +#include + +#include "kernel/handles.hpp" + using namespace Applets; void SoftwareKeyboardApplet::reset() {} -Result::HorizonResult SoftwareKeyboardApplet::start() { return Result::Success; } Result::HorizonResult SoftwareKeyboardApplet::receiveParameter(const Applets::Parameter& parameter) { - Helpers::warn("Software keyboard: Unimplemented ReceiveParameter"); + switch (parameter.signal) { + // Signal == request -> Applet is asking swkbd for a shared memory handle for backing up the framebuffer before opening the applet + case u32(APTSignal::Request): { + Applets::Parameter param = Applets::Parameter{ + .senderID = parameter.destID, + .destID = AppletIDs::Application, + .signal = static_cast(APTSignal::Response), + .object = KernelHandles::APTCaptureSharedMemHandle, + .data = {}, + }; + nextParameter = param; + break; + } + + default: Helpers::panic("Unimplemented swkbd signal %d\n", parameter.signal); + } + + return Result::Success; +} + +Result::HorizonResult SoftwareKeyboardApplet::start(const MemoryBlock* sharedMem, const std::vector& parameters, u32 appID) { + if (parameters.size() < sizeof(SoftwareKeyboardConfig)) { + Helpers::warn("SoftwareKeyboard::Start: Invalid size for keyboard configuration"); + return Result::Success; + } + + if (sharedMem == nullptr) { + Helpers::warn("SoftwareKeyboard: Missing shared memory"); + return Result::Success; + } + + // Get keyboard configuration from the application + std::memcpy(&config, ¶meters[0], sizeof(config)); + + const std::u16string text = u"Pand"; + u32 textAddress = sharedMem->addr; + + // Copy text to shared memory the app gave us + for (u32 i = 0; i < text.size(); i++) { + mem.write16(textAddress, u16(text[i])); + textAddress += sizeof(u16); + } + mem.write16(textAddress, 0); // Write UTF-16 null terminator + + // Temporarily hardcode the pressed button to be the firs tone + switch (config.numButtonsM1) { + case SoftwareKeyboardButtonConfig::SingleButton: config.returnCode = SoftwareKeyboardResult::D0Click; break; + case SoftwareKeyboardButtonConfig::DualButton: config.returnCode = SoftwareKeyboardResult::D1Click1; break; + case SoftwareKeyboardButtonConfig::TripleButton: config.returnCode = SoftwareKeyboardResult::D2Click2; break; + case SoftwareKeyboardButtonConfig::NoButton: config.returnCode = SoftwareKeyboardResult::None; break; + default: Helpers::warn("Software keyboard: Invalid button mode specification"); break; + } + + config.textOffset = 0; + config.textLength = static_cast(text.size()); + static_assert(offsetof(SoftwareKeyboardConfig, textOffset) == 324); + static_assert(offsetof(SoftwareKeyboardConfig, textLength) == 328); + + if (config.filterFlags & SoftwareKeyboardFilter::Callback) { + Helpers::warn("Unimplemented software keyboard profanity callback"); + } + + closeKeyboard(appID); + return Result::Success; +} + +void SoftwareKeyboardApplet::closeKeyboard(u32 appID) { Applets::Parameter param = Applets::Parameter{ - .senderID = parameter.destID, + .senderID = appID, .destID = AppletIDs::Application, - .signal = static_cast(APTSignal::Response), - .data = {}, + .signal = static_cast(APTSignal::WakeupByExit), + .object = 0, }; + // Copy software keyboard configuration into the response parameter + param.data.resize(sizeof(config)); + std::memcpy(¶m.data[0], &config, sizeof(config)); + nextParameter = param; - return Result::Success; } \ No newline at end of file diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp index 83e7cdc4..7b8b71c2 100644 --- a/src/core/cheats.cpp +++ b/src/core/cheats.cpp @@ -1,4 +1,5 @@ #include "cheats.hpp" +#include "swap.hpp" Cheats::Cheats(Memory& mem, HIDService& hid) : ar(mem, hid) { reset(); } @@ -23,6 +24,27 @@ u32 Cheats::addCheat(const Cheat& cheat) { return cheats.size() - 1; } +u32 Cheats::addCheat(const u8* data, size_t size) { + if ((size % 8) != 0) { + return badCheatHandle; + } + + Cheats::Cheat cheat; + cheat.enabled = true; + cheat.type = Cheats::CheatType::ActionReplay; + + for (size_t i = 0; i < size; i += 8) { + auto read32 = [](const u8* ptr) { return (u32(ptr[3]) << 24) | (u32(ptr[2]) << 16) | (u32(ptr[1]) << 8) | u32(ptr[0]); }; + + // Data is passed to us in big endian so we bswap + u32 firstWord = Common::swap32(read32(data + i)); + u32 secondWord = Common::swap32(read32(data + i + 4)); + cheat.instructions.insert(cheat.instructions.end(), {firstWord, secondWord}); + } + + return addCheat(cheat); +} + void Cheats::removeCheat(u32 id) { if (id >= cheats.size()) { return; diff --git a/src/core/fs/archive_sdmc.cpp b/src/core/fs/archive_sdmc.cpp index 232335d0..6c34de7a 100644 --- a/src/core/fs/archive_sdmc.cpp +++ b/src/core/fs/archive_sdmc.cpp @@ -45,8 +45,16 @@ HorizonResult SDMCArchive::deleteFile(const FSPath& path) { FileDescriptor SDMCArchive::openFile(const FSPath& path, const FilePerms& perms) { FilePerms realPerms = perms; - // SD card always has read permission - realPerms.raw |= (1 << 0); + + if (isWriteOnly) { + if (perms.read()) { + Helpers::warn("SDMC: Read flag is not allowed in SDMC Write-Only archive"); + return FileError; + } + } else { + // Regular SDMC archive always has read permission + realPerms.raw |= (1 << 0); + } if ((realPerms.create() && !realPerms.write())) { Helpers::panic("[SDMC] Unsupported flags for OpenFile"); @@ -130,6 +138,11 @@ HorizonResult SDMCArchive::createDirectory(const FSPath& path) { } Rust::Result SDMCArchive::openDirectory(const FSPath& path) { + if (isWriteOnly) { + Helpers::warn("SDMC: OpenDirectory is not allowed in SDMC Write-Only archive"); + return Err(Result::FS::UnexpectedFileOrDir); + } + if (path.type == PathType::UTF16) { if (!isPathSafe(path)) { Helpers::panic("Unsafe path in SaveData::OpenDirectory"); diff --git a/src/core/kernel/events.cpp b/src/core/kernel/events.cpp index 7da4788e..b2f89fbf 100644 --- a/src/core/kernel/events.cpp +++ b/src/core/kernel/events.cpp @@ -126,8 +126,7 @@ void Kernel::waitSynchronization1() { auto& t = threads[currentThreadIndex]; t.waitList.resize(1); t.status = ThreadStatus::WaitSync1; - t.sleepTick = cpu.getTicks(); - t.waitingNanoseconds = ns; + t.wakeupTick = getWakeupTick(ns); t.waitList[0] = handle; // Add the current thread to the object's wait list @@ -220,8 +219,7 @@ void Kernel::waitSynchronizationN() { t.waitList.resize(handleCount); t.status = ThreadStatus::WaitSyncAny; t.outPointer = outPointer; - t.waitingNanoseconds = ns; - t.sleepTick = cpu.getTicks(); + t.wakeupTick = getWakeupTick(ns); for (s32 i = 0; i < handleCount; i++) { t.waitList[i] = waitObjects[i].first; // Add object to this thread's waitlist diff --git a/src/core/kernel/idle_thread.cpp b/src/core/kernel/idle_thread.cpp index 5abba373..d666968b 100644 --- a/src/core/kernel/idle_thread.cpp +++ b/src/core/kernel/idle_thread.cpp @@ -8,13 +8,6 @@ The code for our idle thread looks like this idle_thread_main: - mov r0, #4096 @ Loop counter - - .loop: - nop; nop; nop; nop @ NOP 4 times to waste some cycles - subs r0, #1 @ Decrement counter by 1, go back to looping if loop counter != 0 - bne .loop - // Sleep for 0 seconds with the SleepThread SVC, which just yields execution mov r0, #0 mov r1, #0 @@ -24,14 +17,10 @@ idle_thread_main: */ static constexpr u8 idleThreadCode[] = { - 0x01, 0x0A, 0xA0, 0xE3, // mov r0, #4096 - 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, // nop (4 times) - 0x01, 0x00, 0x50, 0xE2, // subs r0, #1 - 0xF9, 0xFF, 0xFF, 0x1A, // bne loop 0x00, 0x00, 0xA0, 0xE3, // mov r0, #0 0x00, 0x10, 0xA0, 0xE3, // mov r1, #0 0x0A, 0x00, 0x00, 0xEF, // svc SleepThread - 0xF4, 0xFF, 0xFF, 0xEA // b idle_thread_main + 0xFB, 0xFF, 0xFF, 0xEA // b idle_thread_main }; // Set up an idle thread to run when no thread is able to run diff --git a/src/core/kernel/kernel.cpp b/src/core/kernel/kernel.cpp index 2e4fffff..392b87fd 100644 --- a/src/core/kernel/kernel.cpp +++ b/src/core/kernel/kernel.cpp @@ -50,6 +50,7 @@ void Kernel::serviceSVC(u32 svc) { case 0x1D: svcClearTimer(); break; case 0x1E: createMemoryBlock(); break; case 0x1F: mapMemoryBlock(); break; + case 0x20: unmapMemoryBlock(); break; case 0x21: createAddressArbiter(); break; case 0x22: arbitrateAddress(); break; case 0x23: svcCloseHandle(); break; diff --git a/src/core/kernel/memory_management.cpp b/src/core/kernel/memory_management.cpp index e90a5697..0d234be5 100644 --- a/src/core/kernel/memory_management.cpp +++ b/src/core/kernel/memory_management.cpp @@ -144,6 +144,7 @@ void Kernel::mapMemoryBlock() { printf("Mapping CSND memory block\n"); break; + case KernelHandles::APTCaptureSharedMemHandle: break; default: Helpers::panic("Mapping unknown shared memory block: %X", block); } } else { @@ -206,3 +207,12 @@ void Kernel::createMemoryBlock() { regs[0] = Result::Success; regs[1] = makeMemoryBlock(addr, size, myPermission, otherPermission); } + +void Kernel::unmapMemoryBlock() { + Handle block = regs[0]; + u32 addr = regs[1]; + logSVC("Unmap memory block (block handle = %X, addr = %08X)\n", block, addr); + + Helpers::warn("Stubbed svcUnmapMemoryBlock!"); + regs[0] = Result::Success; +} diff --git a/src/core/kernel/threads.cpp b/src/core/kernel/threads.cpp index 239a6617..3a6201c1 100644 --- a/src/core/kernel/threads.cpp +++ b/src/core/kernel/threads.cpp @@ -52,14 +52,8 @@ bool Kernel::canThreadRun(const Thread& t) { return true; } else if (t.status == ThreadStatus::WaitSleep || t.status == ThreadStatus::WaitSync1 || t.status == ThreadStatus::WaitSyncAny || t.status == ThreadStatus::WaitSyncAll) { - const u64 elapsedTicks = cpu.getTicks() - t.sleepTick; - - constexpr double ticksPerSec = double(CPU::ticksPerSec); - constexpr double nsPerTick = ticksPerSec / 1000000000.0; - // TODO: Set r0 to the correct error code on timeout for WaitSync{1/Any/All} - const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick); - return elapsedNs >= t.waitingNanoseconds; + return cpu.getTicks() >= t.wakeupTick; } // Handle timeouts and stuff here @@ -82,6 +76,15 @@ std::optional Kernel::getNextThread() { return std::nullopt; } +u64 Kernel::getWakeupTick(s64 ns) { + // Timeout == -1 means that the thread doesn't plan on waking up automatically + if (ns == -1) { + return std::numeric_limits::max(); + } + + return cpu.getTicks() + Scheduler::nsToCycles(ns); +} + // See if there is a higher priority, ready thread and switch to that void Kernel::rescheduleThreads() { Thread& current = threads[currentThreadIndex]; // Current running thread @@ -368,13 +371,30 @@ void Kernel::sleepThread(s64 ns) { if (index != idleThreadIndex) { switchThread(index); } + } else { + if (currentThreadIndex == idleThreadIndex) { + const Scheduler& scheduler = cpu.getScheduler(); + u64 timestamp = scheduler.nextTimestamp; + + for (auto i : threadIndices) { + const Thread& t = threads[i]; + if (t.status == ThreadStatus::WaitSleep || t.status == ThreadStatus::WaitSync1 || t.status == ThreadStatus::WaitSyncAny || + t.status == ThreadStatus::WaitSyncAll) { + timestamp = std::min(timestamp, t.wakeupTick); + } + } + + if (timestamp > scheduler.currentTimestamp) { + u64 idleCycles = timestamp - scheduler.currentTimestamp; + cpu.addTicks(idleCycles); + } + } } } else { // If we're sleeping for >= 0 ns Thread& t = threads[currentThreadIndex]; t.status = ThreadStatus::WaitSleep; - t.waitingNanoseconds = ns; - t.sleepTick = cpu.getTicks(); + t.wakeupTick = getWakeupTick(ns); requireReschedule(); } diff --git a/src/core/kernel/timers.cpp b/src/core/kernel/timers.cpp index a9c95292..35fc57a4 100644 --- a/src/core/kernel/timers.cpp +++ b/src/core/kernel/timers.cpp @@ -1,5 +1,8 @@ -#include "kernel.hpp" +#include + #include "cpu.hpp" +#include "kernel.hpp" +#include "scheduler.hpp" Handle Kernel::makeTimer(ResetType type) { Handle ret = makeObject(KernelObjectType::Timer); @@ -13,27 +16,44 @@ Handle Kernel::makeTimer(ResetType type) { return ret; } -void Kernel::updateTimer(Handle handle, Timer* timer) { - if (timer->running) { - const u64 currentTicks = cpu.getTicks(); - u64 elapsedTicks = currentTicks - timer->startTick; +void Kernel::pollTimers() { + u64 currentTick = cpu.getTicks(); - constexpr double ticksPerSec = double(CPU::ticksPerSec); - constexpr double nsPerTick = ticksPerSec / 1000000000.0; - const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick); + // Find the next timestamp we'll poll KTimers on. To do this, we find the minimum tick one of our timers will fire + u64 nextTimestamp = std::numeric_limits::max(); + // Do we have any active timers anymore? If not, then we won't need to schedule a new timer poll event + bool haveActiveTimers = false; - // Timer has fired - if (elapsedNs >= timer->currentDelay) { - timer->startTick = currentTicks; - timer->currentDelay = timer->interval; - signalTimer(handle, timer); + for (auto handle : timerHandles) { + KernelObject* object = getObject(handle, KernelObjectType::Timer); + if (object != nullptr) { + Timer* timer = object->getData(); + + if (timer->running) { + // If timer has fired, signal it and set the tick it will next time + if (currentTick >= timer->fireTick) { + signalTimer(handle, timer); + } + + // Update our next timer fire timestamp and mark that we should schedule a new event to poll timers + // We recheck timer->running because signalling a timer stops it if interval == 0 + if (timer->running) { + nextTimestamp = std::min(nextTimestamp, timer->fireTick); + haveActiveTimers = true; + } + } } } + + // If we still have active timers, schedule next poll event + if (haveActiveTimers) { + Scheduler& scheduler = cpu.getScheduler(); + scheduler.addEvent(Scheduler::EventType::UpdateTimers, nextTimestamp); + } } void Kernel::cancelTimer(Timer* timer) { timer->running = false; - // TODO: When we have a scheduler this should properly cancel timer events in the scheduler } void Kernel::signalTimer(Handle timerHandle, Timer* timer) { @@ -54,6 +74,8 @@ void Kernel::signalTimer(Handle timerHandle, Timer* timer) { if (timer->interval == 0) { cancelTimer(timer); + } else { + timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(timer->interval); } } @@ -87,18 +109,20 @@ void Kernel::svcSetTimer() { Timer* timer = object->getData(); cancelTimer(timer); - timer->currentDelay = initial; timer->interval = interval; timer->running = true; - timer->startTick = cpu.getTicks(); + timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(initial); + + Scheduler& scheduler = cpu.getScheduler(); + // Signal an event to poll timers as soon as possible + scheduler.removeEvent(Scheduler::EventType::UpdateTimers); + scheduler.addEvent(Scheduler::EventType::UpdateTimers, cpu.getTicks() + 1); // If the initial delay is 0 then instantly signal the timer if (initial == 0) { signalTimer(handle, timer); - } else { - // This should schedule an event in the scheduler when we have one } - + regs[0] = Result::Success; } diff --git a/src/core/services/apt.cpp b/src/core/services/apt.cpp index 830b8377..404a0e59 100644 --- a/src/core/services/apt.cpp +++ b/src/core/services/apt.cpp @@ -64,6 +64,7 @@ void APTService::handleSyncRequest(u32 messagePointer) { case APTCommands::NotifyToWait: notifyToWait(messagePointer); break; case APTCommands::PreloadLibraryApplet: preloadLibraryApplet(messagePointer); break; case APTCommands::PrepareToStartLibraryApplet: prepareToStartLibraryApplet(messagePointer); break; + case APTCommands::StartLibraryApplet: startLibraryApplet(messagePointer); break; case APTCommands::ReceiveParameter: [[likely]] receiveParameter(messagePointer); break; case APTCommands::ReplySleepQuery: replySleepQuery(messagePointer); break; case APTCommands::SetApplicationCpuTimeLimit: setApplicationCpuTimeLimit(messagePointer); break; @@ -140,6 +141,39 @@ void APTService::prepareToStartLibraryApplet(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void APTService::startLibraryApplet(u32 messagePointer) { + const u32 appID = mem.read32(messagePointer + 4); + const u32 bufferSize = mem.read32(messagePointer + 8); + const Handle parameters = mem.read32(messagePointer + 16); + const u32 buffer = mem.read32(messagePointer + 24); + log("APT::StartLibraryApplet (app ID = %X)\n", appID); + + Applets::AppletBase* destApplet = appletManager.getApplet(appID); + if (destApplet == nullptr) { + Helpers::warn("APT::StartLibraryApplet: Unimplemented dest applet ID"); + mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); + } else { + KernelObject* sharedMemObject = kernel.getObject(parameters); + + const MemoryBlock* sharedMem = sharedMemObject ? sharedMemObject->getData() : nullptr; + std::vector data; + data.reserve(bufferSize); + + for (u32 i = 0; i < bufferSize; i++) { + data.push_back(mem.read8(buffer + i)); + } + + Result::HorizonResult result = destApplet->start(sharedMem, data, appID); + if (resumeEvent.has_value()) { + kernel.signalEvent(resumeEvent.value()); + } + + mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 0)); + mem.write32(messagePointer + 4, result); + } +} + void APTService::checkNew3DS(u32 messagePointer) { log("APT::CheckNew3DS\n"); mem.write32(messagePointer, IPC::responseHeader(0x102, 2, 0)); @@ -222,7 +256,7 @@ void APTService::sendParameter(u32 messagePointer) { const u32 parameterHandle = mem.read32(messagePointer + 24); // What dis? const u32 parameterPointer = mem.read32(messagePointer + 32); - log("APT::SendParameter (source app = %X, dest app = %X, cmd = %X, size = %X) (Stubbed)", sourceAppID, destAppID, cmd, paramSize); + log("APT::SendParameter (source app = %X, dest app = %X, cmd = %X, size = %X)", sourceAppID, destAppID, cmd, paramSize); mem.write32(messagePointer, IPC::responseHeader(0x0C, 1, 0)); mem.write32(messagePointer + 4, Result::Success); @@ -260,7 +294,9 @@ void APTService::sendParameter(u32 messagePointer) { void APTService::receiveParameter(u32 messagePointer) { const u32 app = mem.read32(messagePointer + 4); const u32 size = mem.read32(messagePointer + 8); - log("APT::ReceiveParameter(app ID = %X, size = %04X) (STUBBED)\n", app, size); + // Parameter data pointer is in the thread static buffer, which starts 0x100 bytes after the command buffer + const u32 buffer = mem.read32(messagePointer + 0x100 + 4); + log("APT::ReceiveParameter(app ID = %X, size = %04X)\n", app, size); if (size > 0x1000) Helpers::panic("APT::ReceiveParameter with size > 0x1000"); auto parameter = appletManager.receiveParameter(); @@ -274,14 +310,21 @@ void APTService::receiveParameter(u32 messagePointer) { // Size of parameter data mem.write32(messagePointer + 16, parameter.data.size()); mem.write32(messagePointer + 20, 0x10); - mem.write32(messagePointer + 24, 0); + mem.write32(messagePointer + 24, parameter.object); mem.write32(messagePointer + 28, 0); + + const u32 transferSize = std::min(size, parameter.data.size()); + for (u32 i = 0; i < transferSize; i++) { + mem.write8(buffer + i, parameter.data[i]); + } } void APTService::glanceParameter(u32 messagePointer) { const u32 app = mem.read32(messagePointer + 4); const u32 size = mem.read32(messagePointer + 8); - log("APT::GlanceParameter(app ID = %X, size = %04X) (STUBBED)\n", app, size); + // Parameter data pointer is in the thread static buffer, which starts 0x100 bytes after the command buffer + const u32 buffer = mem.read32(messagePointer + 0x100 + 4); + log("APT::GlanceParameter(app ID = %X, size = %04X)\n", app, size); if (size > 0x1000) Helpers::panic("APT::GlanceParameter with size > 0x1000"); auto parameter = appletManager.glanceParameter(); @@ -296,8 +339,13 @@ void APTService::glanceParameter(u32 messagePointer) { // Size of parameter data mem.write32(messagePointer + 16, parameter.data.size()); mem.write32(messagePointer + 20, 0); - mem.write32(messagePointer + 24, 0); + mem.write32(messagePointer + 24, parameter.object); mem.write32(messagePointer + 28, 0); + + const u32 transferSize = std::min(size, parameter.data.size()); + for (u32 i = 0; i < transferSize; i++) { + mem.write8(buffer + i, parameter.data[i]); + } } void APTService::replySleepQuery(u32 messagePointer) { diff --git a/src/core/services/boss.cpp b/src/core/services/boss.cpp index 30190cd3..a8f7194b 100644 --- a/src/core/services/boss.cpp +++ b/src/core/services/boss.cpp @@ -6,6 +6,7 @@ namespace BOSSCommands { InitializeSession = 0x00010082, UnregisterStorage = 0x00030000, GetTaskStorageInfo = 0x00040000, + GetNewArrivalFlag = 0x00070000, RegisterNewArrivalEvent = 0x00080002, SetOptoutFlag = 0x00090040, GetOptoutFlag = 0x000A0000, @@ -37,6 +38,7 @@ void BOSSService::handleSyncRequest(u32 messagePointer) { switch (command) { case BOSSCommands::CancelTask: cancelTask(messagePointer); break; case BOSSCommands::GetErrorCode: getErrorCode(messagePointer); break; + case BOSSCommands::GetNewArrivalFlag: getNewArrivalFlag(messagePointer); break; case BOSSCommands::GetNsDataIdList: case BOSSCommands::GetNsDataIdList1: getNsDataIdList(messagePointer, command); break; @@ -240,4 +242,11 @@ void BOSSService::unregisterStorage(u32 messagePointer) { log("BOSS::UnregisterStorage (stubbed)\n"); mem.write32(messagePointer, IPC::responseHeader(0x3, 1, 0)); mem.write32(messagePointer + 4, Result::Success); +} + +void BOSSService::getNewArrivalFlag(u32 messagePointer) { + log("BOSS::GetNewArrivalFlag (stubbed)\n"); + mem.write32(messagePointer, IPC::responseHeader(0x7, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + mem.write8(messagePointer + 8, 0); // Flag } \ No newline at end of file diff --git a/src/core/services/cam.cpp b/src/core/services/cam.cpp index a0206077..b3dfd1dc 100644 --- a/src/core/services/cam.cpp +++ b/src/core/services/cam.cpp @@ -1,31 +1,96 @@ #include "services/cam.hpp" + +#include + #include "ipc.hpp" #include "kernel.hpp" namespace CAMCommands { enum : u32 { + StartCapture = 0x00010040, GetBufferErrorInterruptEvent = 0x00060040, + SetReceiving = 0x00070102, DriverInitialize = 0x00390000, + DriverFinalize = 0x003A0000, SetTransferLines = 0x00090100, GetMaxLines = 0x000A0080, + SetTransferBytes = 0x000B0100, + GetTransferBytes = 0x000C0040, + GetMaxBytes = 0x000D0080, + SetTrimming = 0x000E0080, + SetTrimmingParamsCenter = 0x00120140, + SetSize = 0x001F00C0, // Set size has different headers between cam:u and New3DS QTM module SetFrameRate = 0x00200080, SetContrast = 0x00230080, + GetSuitableY2rStandardCoefficient = 0x00360000, }; } -void CAMService::reset() { bufferErrorInterruptEvents.fill(std::nullopt); } +// Helper struct for working with camera ports +class PortSelect { + u32 value; + + public: + PortSelect(u32 val) : value(val) {} + bool isValid() const { return value < 4; } + + bool isSinglePort() const { + // 1 corresponds to the first camera port and 2 corresponds to the second port + return value == 1 || value == 2; + } + + bool isBothPorts() const { + // 3 corresponds to both ports + return value == 3; + } + + // Returns the index of the camera port, assuming that it's only a single port + int getSingleIndex() const { + if (!isSinglePort()) [[unlikely]] { + Helpers::panic("Camera: getSingleIndex called for port with invalid value"); + } + + return value - 1; + } + + std::vector getPortIndices() const { + switch (value) { + case 1: return {0}; // Only port 1 + case 2: return {1}; // Only port 2 + case 3: return {0, 1}; // Both port 1 and port 2 + default: return {}; // No ports or invalid ports + } + } +}; + +void CAMService::reset() { + for (auto& port : ports) { + port.reset(); + } +} void CAMService::handleSyncRequest(u32 messagePointer) { const u32 command = mem.read32(messagePointer); switch (command) { case CAMCommands::DriverInitialize: driverInitialize(messagePointer); break; + case CAMCommands::DriverFinalize: driverFinalize(messagePointer); break; case CAMCommands::GetBufferErrorInterruptEvent: getBufferErrorInterruptEvent(messagePointer); break; + case CAMCommands::GetMaxBytes: getMaxBytes(messagePointer); break; case CAMCommands::GetMaxLines: getMaxLines(messagePointer); break; + case CAMCommands::GetSuitableY2rStandardCoefficient: getSuitableY2RCoefficients(messagePointer); break; + case CAMCommands::GetTransferBytes: getTransferBytes(messagePointer); break; case CAMCommands::SetContrast: setContrast(messagePointer); break; case CAMCommands::SetFrameRate: setFrameRate(messagePointer); break; + case CAMCommands::SetReceiving: setReceiving(messagePointer); break; + case CAMCommands::SetSize: setSize(messagePointer); break; case CAMCommands::SetTransferLines: setTransferLines(messagePointer); break; + case CAMCommands::SetTrimming: setTrimming(messagePointer); break; + case CAMCommands::SetTrimmingParamsCenter: setTrimmingParamsCenter(messagePointer); break; + case CAMCommands::StartCapture: startCapture(messagePointer); break; + default: - Helpers::panic("Unimplemented CAM service requested. Command: %08X\n", command); + Helpers::warn("Unimplemented CAM service requested. Command: %08X\n", command); + mem.write32(messagePointer + 4, Result::Success); break; } } @@ -36,6 +101,12 @@ void CAMService::driverInitialize(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void CAMService::driverFinalize(u32 messagePointer) { + log("CAM::DriverFinalize\n"); + mem.write32(messagePointer, IPC::responseHeader(0x3A, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + void CAMService::setContrast(u32 messagePointer) { const u32 cameraSelect = mem.read32(messagePointer + 4); const u32 contrast = mem.read32(messagePointer + 8); @@ -46,13 +117,46 @@ void CAMService::setContrast(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } -void CAMService::setTransferLines(u32 messagePointer) { - const u32 port = mem.read32(messagePointer + 4); - const s16 lines = mem.read16(messagePointer + 8); - const s16 width = mem.read16(messagePointer + 12); - const s16 height = mem.read16(messagePointer + 16); +void CAMService::setTransferBytes(u32 messagePointer) { + const u32 portIndex = mem.read8(messagePointer + 4); + const u32 bytes = mem.read16(messagePointer + 8); + // ...why do these parameters even exist? + const u16 width = mem.read16(messagePointer + 12); + const u16 height = mem.read16(messagePointer + 16); + const PortSelect port(portIndex); - log("CAM::SetTransferLines (port = %d, lines = %d, width = %d, height = %d)\n", port, lines, width, height); + if (port.isValid()) { + for (int i : port.getPortIndices()) { + ports[i].transferBytes = bytes; + } + } else { + Helpers::warn("CAM::SetTransferBytes: Invalid port\n"); + } + + log("CAM::SetTransferBytes (port = %d, bytes = %d, width = %d, height = %d)\n", portIndex, bytes, width, height); + + mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + +void CAMService::setTransferLines(u32 messagePointer) { + const u32 portIndex = mem.read8(messagePointer + 4); + const u16 lines = mem.read16(messagePointer + 8); + const u16 width = mem.read16(messagePointer + 12); + const u16 height = mem.read16(messagePointer + 16); + const PortSelect port(portIndex); + + if (port.isValid()) { + const u32 transferBytes = lines * width * 2; + + for (int i : port.getPortIndices()) { + ports[i].transferBytes = transferBytes; + } + } else { + Helpers::warn("CAM::SetTransferLines: Invalid port\n"); + } + + log("CAM::SetTransferLines (port = %d, lines = %d, width = %d, height = %d)\n", portIndex, lines, width, height); mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0)); mem.write32(messagePointer + 4, Result::Success); @@ -68,6 +172,41 @@ void CAMService::setFrameRate(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void CAMService::setSize(u32 messagePointer) { + const u32 cameraSelect = mem.read32(messagePointer + 4); + const u32 size = mem.read32(messagePointer + 8); + const u32 context = mem.read32(messagePointer + 12); + + log("CAM::SetSize (camera select = %d, size = %d, context = %d)\n", cameraSelect, size, context); + + mem.write32(messagePointer, IPC::responseHeader(0x1F, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + +void CAMService::setTrimming(u32 messagePointer) { + const u32 port = mem.read8(messagePointer + 4); + const bool trim = mem.read8(messagePointer + 8) != 0; + + log("CAM::SetTrimming (port = %d, trimming = %s)\n", port, trim ? "enabled" : "disabled"); + + mem.write32(messagePointer, IPC::responseHeader(0x0E, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + +void CAMService::setTrimmingParamsCenter(u32 messagePointer) { + const u32 port = mem.read8(messagePointer + 4); + const s16 trimWidth = s16(mem.read16(messagePointer + 8)); + const s16 trimHeight = s16(mem.read16(messagePointer + 12)); + const s16 cameraWidth = s16(mem.read16(messagePointer + 16)); + const s16 cameraHeight = s16(mem.read16(messagePointer + 20)); + + log("CAM::SetTrimmingParamsCenter (port = %d), trim size = (%d, %d), camera size = (%d, %d)\n", port, trimWidth, trimHeight, cameraWidth, + cameraHeight); + + mem.write32(messagePointer, IPC::responseHeader(0x12, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + // Algorithm taken from Citra // https://github.com/citra-emu/citra/blob/master/src/core/hle/service/cam/cam.cpp#L465 void CAMService::getMaxLines(u32 messagePointer) { @@ -100,16 +239,62 @@ void CAMService::getMaxLines(u32 messagePointer) { } } +void CAMService::getMaxBytes(u32 messagePointer) { + const u16 width = mem.read16(messagePointer + 4); + const u16 height = mem.read16(messagePointer + 8); + log("CAM::GetMaxBytes (width = %d, height = %d)\n", width, height); + + constexpr u32 MIN_TRANSFER_UNIT = 256; + constexpr u32 MAX_BUFFER_SIZE = 2560; + if (width * height * 2 % MIN_TRANSFER_UNIT != 0) { + Helpers::panic("CAM::GetMaxLines out of range"); + } else { + u32 bytes = MAX_BUFFER_SIZE; + + while (width * height * 2 % bytes != 0) { + bytes -= MIN_TRANSFER_UNIT; + } + + mem.write32(messagePointer, IPC::responseHeader(0xA, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + mem.write32(messagePointer + 8, bytes); + } +} + +void CAMService::getSuitableY2RCoefficients(u32 messagePointer) { + log("CAM::GetSuitableY2RCoefficients\n"); + mem.write32(messagePointer, IPC::responseHeader(0x36, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + // Y2R standard coefficient value + mem.write32(messagePointer + 8, 0); +} + +void CAMService::getTransferBytes(u32 messagePointer) { + const u32 portIndex = mem.read8(messagePointer + 4); + const PortSelect port(portIndex); + log("CAM::GetTransferBytes (port = %d)\n", portIndex); + + mem.write32(messagePointer, IPC::responseHeader(0x0C, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + + if (port.isSinglePort()) { + mem.write32(messagePointer + 8, ports[port.getSingleIndex()].transferBytes); + } else { + // TODO: This should return the proper error code + Helpers::warn("CAM::GetTransferBytes: Invalid port index"); + mem.write32(messagePointer + 8, 0); + } +} + void CAMService::getBufferErrorInterruptEvent(u32 messagePointer) { - const u32 port = mem.read32(messagePointer + 4); - log("CAM::GetBufferErrorInterruptEvent (port = %d)\n", port); + const u32 portIndex = mem.read8(messagePointer + 4); + const PortSelect port(portIndex); + log("CAM::GetBufferErrorInterruptEvent (port = %d)\n", portIndex); mem.write32(messagePointer, IPC::responseHeader(0x6, 1, 2)); - if (port >= portCount) { - Helpers::panic("CAM::GetBufferErrorInterruptEvent: Invalid port"); - } else { - auto& event = bufferErrorInterruptEvents[port]; + if (port.isSinglePort()) { + auto& event = ports[port.getSingleIndex()].bufferErrorInterruptEvent; if (!event.has_value()) { event = kernel.makeEvent(ResetType::OneShot); } @@ -117,5 +302,55 @@ void CAMService::getBufferErrorInterruptEvent(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); mem.write32(messagePointer + 8, 0); mem.write32(messagePointer + 12, event.value()); + } else { + Helpers::panic("CAM::GetBufferErrorInterruptEvent: Invalid port"); } -} \ No newline at end of file +} + +void CAMService::setReceiving(u32 messagePointer) { + const u32 destination = mem.read32(messagePointer + 4); + const u32 portIndex = mem.read8(messagePointer + 8); + const u32 size = mem.read32(messagePointer + 12); + const u16 transferUnit = mem.read16(messagePointer + 16); + const Handle process = mem.read32(messagePointer + 24); + + const PortSelect port(portIndex); + log("CAM::SetReceiving (port = %d)\n", portIndex); + + mem.write32(messagePointer, IPC::responseHeader(0x7, 1, 2)); + + if (port.isSinglePort()) { + auto& event = ports[port.getSingleIndex()].receiveEvent; + if (!event.has_value()) { + event = kernel.makeEvent(ResetType::OneShot); + } + + mem.write32(messagePointer + 4, Result::Success); + mem.write32(messagePointer + 8, 0); + mem.write32(messagePointer + 12, event.value()); + } else { + Helpers::panic("CAM::SetReceiving: Invalid port"); + } +} + +void CAMService::startCapture(u32 messagePointer) { + const u32 portIndex = mem.read8(messagePointer + 4); + const PortSelect port(portIndex); + log("CAM::StartCapture (port = %d)\n", portIndex); + + mem.write32(messagePointer, IPC::responseHeader(0x01, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); + + if (port.isValid()) { + for (int i : port.getPortIndices()) { + auto& event = ports[port.getSingleIndex()].receiveEvent; + + // Until we properly implement cameras, immediately signal the receive event + if (event.has_value()) { + kernel.signalEvent(event.value()); + } + } + } else { + Helpers::warn("CAM::StartCapture: Invalid port index"); + } +} diff --git a/src/core/services/fs.cpp b/src/core/services/fs.cpp index 1f3317fb..2e102958 100644 --- a/src/core/services/fs.cpp +++ b/src/core/services/fs.cpp @@ -27,6 +27,7 @@ namespace FSCommands { CloseArchive = 0x080E0080, FormatThisUserSaveData = 0x080F0180, GetFreeBytes = 0x08120080, + GetSdmcArchiveResource = 0x08140000, IsSdmcDetected = 0x08170000, IsSdmcWritable = 0x08180000, CardSlotIsInserted = 0x08210000, @@ -96,6 +97,7 @@ ArchiveBase* FSService::getArchiveFromID(u32 id, const FSPath& archivePath) { case ArchiveID::SystemSaveData: return &systemSaveData; case ArchiveID::SDMC: return &sdmc; + case ArchiveID::SDMCWriteOnly: return &sdmcWriteOnly; case ArchiveID::SavedataAndNcch: return &ncch; // This can only access NCCH outside of FSPXI default: Helpers::panic("Unknown archive. ID: %d\n", id); @@ -179,6 +181,7 @@ void FSService::handleSyncRequest(u32 messagePointer) { case FSCommands::GetFreeBytes: getFreeBytes(messagePointer); break; case FSCommands::GetFormatInfo: getFormatInfo(messagePointer); break; case FSCommands::GetPriority: getPriority(messagePointer); break; + case FSCommands::GetSdmcArchiveResource: getSdmcArchiveResource(messagePointer); break; case FSCommands::GetThisSaveDataSecureValue: getThisSaveDataSecureValue(messagePointer); break; case FSCommands::Initialize: initialize(messagePointer); break; case FSCommands::InitializeWithSdkVersion: initializeWithSdkVersion(messagePointer); break; @@ -764,3 +767,22 @@ void FSService::renameFile(u32 messagePointer) { const HorizonResult res = sourceArchive->archive->renameFile(sourcePath, destPath); mem.write32(messagePointer + 4, static_cast(res)); } + +void FSService::getSdmcArchiveResource(u32 messagePointer) { + log("FS::GetSdmcArchiveResource"); // For the time being, return the same stubbed archive resource for every media type + + static constexpr ArchiveResource resource = { + .sectorSize = 512, + .clusterSize = 16_KB, + .partitionCapacityInClusters = 0x80000, // 0x80000 * 16 KB = 8GB + .freeSpaceInClusters = 0x80000, // Same here + }; + + mem.write32(messagePointer, IPC::responseHeader(0x814, 5, 0)); + mem.write32(messagePointer + 4, Result::Success); + + mem.write32(messagePointer + 8, resource.sectorSize); + mem.write32(messagePointer + 12, resource.clusterSize); + mem.write32(messagePointer + 16, resource.partitionCapacityInClusters); + mem.write32(messagePointer + 20, resource.freeSpaceInClusters); +} \ No newline at end of file diff --git a/src/core/services/gsp_gpu.cpp b/src/core/services/gsp_gpu.cpp index 861dfb0a..8dff6a79 100644 --- a/src/core/services/gsp_gpu.cpp +++ b/src/core/services/gsp_gpu.cpp @@ -18,6 +18,7 @@ namespace ServiceCommands { ReleaseRight = 0x00170000, ImportDisplayCaptureInfo = 0x00180000, SaveVramSysArea = 0x00190000, + RestoreVramSysArea = 0x001A0000, SetInternalPriorities = 0x001E0080, StoreDataCache = 0x001F0082 }; @@ -51,6 +52,7 @@ void GPUService::handleSyncRequest(u32 messagePointer) { case ServiceCommands::ImportDisplayCaptureInfo: importDisplayCaptureInfo(messagePointer); break; case ServiceCommands::RegisterInterruptRelayQueue: registerInterruptRelayQueue(messagePointer); break; case ServiceCommands::ReleaseRight: releaseRight(messagePointer); break; + case ServiceCommands::RestoreVramSysArea: restoreVramSysArea(messagePointer); break; case ServiceCommands::SaveVramSysArea: saveVramSysArea(messagePointer); break; case ServiceCommands::SetAxiConfigQoSMode: setAxiConfigQoSMode(messagePointer); break; case ServiceCommands::SetBufferSwap: setBufferSwap(messagePointer); break; @@ -143,8 +145,7 @@ void GPUService::requestInterrupt(GPUInterrupt type) { // Not emulating this causes Yoshi's Wooly World, Captain Toad, Metroid 2 et al to hang if (type == GPUInterrupt::VBlank0 || type == GPUInterrupt::VBlank1) { int screen = static_cast(type) - static_cast(GPUInterrupt::VBlank0); // 0 for top screen, 1 for bottom - // TODO: Offset depends on GSP thread being triggered - FramebufferUpdate* update = reinterpret_cast(&sharedMem[0x200 + screen * sizeof(FramebufferUpdate)]); + FramebufferUpdate* update = getFramebufferInfo(screen); if (update->dirtyFlag & 1) { setBufferSwapImpl(screen, update->framebufferInfo[update->index]); @@ -482,10 +483,50 @@ void GPUService::saveVramSysArea(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void GPUService::restoreVramSysArea(u32 messagePointer) { + Helpers::warn("GSP::GPU::RestoreVramSysArea (stubbed)"); + + mem.write32(messagePointer, IPC::responseHeader(0x1A, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + // Used in similar fashion to the SaveVramSysArea function void GPUService::importDisplayCaptureInfo(u32 messagePointer) { Helpers::warn("GSP::GPU::ImportDisplayCaptureInfo (stubbed)"); mem.write32(messagePointer, IPC::responseHeader(0x18, 9, 0)); mem.write32(messagePointer + 4, Result::Success); + + if (sharedMem == nullptr) { + Helpers::warn("GSP::GPU::ImportDisplayCaptureInfo called without GSP module being properly initialized!"); + return; + } + + FramebufferUpdate* topScreen = getTopFramebufferInfo(); + FramebufferUpdate* bottomScreen = getBottomFramebufferInfo(); + + // Capture the relevant data for both screens and return them to the caller + CaptureInfo topScreenCapture = { + .leftFramebuffer = topScreen->framebufferInfo[topScreen->index].leftFramebufferVaddr, + .rightFramebuffer = topScreen->framebufferInfo[topScreen->index].rightFramebufferVaddr, + .format = topScreen->framebufferInfo[topScreen->index].format, + .stride = topScreen->framebufferInfo[topScreen->index].stride, + }; + + CaptureInfo bottomScreenCapture = { + .leftFramebuffer = bottomScreen->framebufferInfo[bottomScreen->index].leftFramebufferVaddr, + .rightFramebuffer = bottomScreen->framebufferInfo[bottomScreen->index].rightFramebufferVaddr, + .format = bottomScreen->framebufferInfo[bottomScreen->index].format, + .stride = bottomScreen->framebufferInfo[bottomScreen->index].stride, + }; + + mem.write32(messagePointer + 8, topScreenCapture.leftFramebuffer); + mem.write32(messagePointer + 12, topScreenCapture.rightFramebuffer); + mem.write32(messagePointer + 16, topScreenCapture.format); + mem.write32(messagePointer + 20, topScreenCapture.stride); + + mem.write32(messagePointer + 24, bottomScreenCapture.leftFramebuffer); + 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/ptm.cpp b/src/core/services/ptm.cpp index 071fa012..67451cc2 100644 --- a/src/core/services/ptm.cpp +++ b/src/core/services/ptm.cpp @@ -6,6 +6,7 @@ namespace PTMCommands { GetAdapterState = 0x00050000, GetBatteryLevel = 0x00070000, GetBatteryChargeState = 0x00080000, + GetPedometerState = 0x00090000, GetStepHistory = 0x000B00C2, GetTotalStepCount = 0x000C0000, GetStepHistoryAll = 0x000F0084, @@ -30,6 +31,7 @@ void PTMService::handleSyncRequest(u32 messagePointer, PTMService::Type type) { case PTMCommands::GetAdapterState: getAdapterState(messagePointer); break; case PTMCommands::GetBatteryChargeState: getBatteryChargeState(messagePointer); break; case PTMCommands::GetBatteryLevel: getBatteryLevel(messagePointer); break; + case PTMCommands::GetPedometerState: getPedometerState(messagePointer); break; case PTMCommands::GetStepHistory: getStepHistory(messagePointer); break; case PTMCommands::GetStepHistoryAll: getStepHistoryAll(messagePointer); break; case PTMCommands::GetTotalStepCount: getTotalStepCount(messagePointer); break; @@ -67,11 +69,20 @@ void PTMService::getBatteryChargeState(u32 messagePointer) { // We're only charging if the battery is not already full const bool charging = config.chargerPlugged && (config.batteryPercentage < 100); - mem.write32(messagePointer, IPC::responseHeader(0x7, 2, 0)); + mem.write32(messagePointer, IPC::responseHeader(0x8, 2, 0)); mem.write32(messagePointer + 4, Result::Success); mem.write8(messagePointer + 8, charging ? 1 : 0); } +void PTMService::getPedometerState(u32 messagePointer) { + log("PTM::GetPedometerState"); + constexpr bool countingSteps = true; + + mem.write32(messagePointer, IPC::responseHeader(0x9, 2, 0)); + mem.write32(messagePointer + 4, Result::Success); + mem.write8(messagePointer + 8, countingSteps ? 1 : 0); +} + void PTMService::getBatteryLevel(u32 messagePointer) { log("PTM::GetBatteryLevel"); diff --git a/src/core/services/y2r.cpp b/src/core/services/y2r.cpp index b5daf6bb..99b18418 100644 --- a/src/core/services/y2r.cpp +++ b/src/core/services/y2r.cpp @@ -18,6 +18,7 @@ namespace Y2RCommands { SetSendingY = 0x00100102, SetSendingU = 0x00110102, SetSendingV = 0x00120102, + SetSendingYUV = 0x00130102, SetReceiving = 0x00180102, SetInputLineWidth = 0x001A0040, GetInputLineWidth = 0x001B0000, @@ -82,6 +83,7 @@ void Y2RService::handleSyncRequest(u32 messagePointer) { case Y2RCommands::SetSendingY: setSendingY(messagePointer); break; case Y2RCommands::SetSendingU: setSendingU(messagePointer); break; case Y2RCommands::SetSendingV: setSendingV(messagePointer); break; + case Y2RCommands::SetSendingYUV: setSendingYUV(messagePointer); break; case Y2RCommands::SetSpacialDithering: setSpacialDithering(messagePointer); break; case Y2RCommands::SetStandardCoeff: setStandardCoeff(messagePointer); break; case Y2RCommands::SetTemporalDithering: setTemporalDithering(messagePointer); break; @@ -399,6 +401,14 @@ void Y2RService::setSendingV(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); } +void Y2RService::setSendingYUV(u32 messagePointer) { + log("Y2R::SetSendingYUV\n"); + Helpers::warn("Unimplemented Y2R::SetSendingYUV"); + + mem.write32(messagePointer, IPC::responseHeader(0x13, 1, 0)); + mem.write32(messagePointer + 4, Result::Success); +} + void Y2RService::setReceiving(u32 messagePointer) { log("Y2R::SetReceiving\n"); Helpers::warn("Unimplemented Y2R::setReceiving"); diff --git a/src/emulator.cpp b/src/emulator.cpp index e94170a2..c567cbc7 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -17,10 +17,11 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1; #endif Emulator::Emulator() - : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel), gpu(memory, config), - memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false) + : 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) #ifdef PANDA3DS_ENABLE_HTTP_SERVER - , httpServer(this) + , + httpServer(this) #endif { #ifdef PANDA3DS_ENABLE_DISCORD_RPC @@ -45,6 +46,9 @@ void Emulator::reset(ReloadOption reload) { cpu.reset(); gpu.reset(); memory.reset(); + // Reset scheduler and add a VBlank event + scheduler.reset(); + // Kernel must be reset last because it depends on CPU/Memory state kernel.reset(); @@ -99,12 +103,6 @@ void Emulator::runFrame() { if (running) { cpu.runFrame(); // Run 1 frame of instructions gpu.display(); // Display graphics - lua.signalEvent(LuaEvent::Frame); - - // Send VBlank interrupts - ServiceManager& srv = kernel.getServiceManager(); - srv.sendGPUInterrupt(GPUInterrupt::VBlank0); - srv.sendGPUInterrupt(GPUInterrupt::VBlank1); // Run cheats if any are loaded if (cheats.haveCheats()) [[unlikely]] { @@ -117,6 +115,67 @@ void Emulator::runFrame() { } } +void Emulator::pollScheduler() { + auto& events = scheduler.events; + + // Pop events until there's none pending anymore + while (scheduler.currentTimestamp >= scheduler.nextTimestamp) { + // Read event timestamp and type, pop it from the scheduler and handle it + auto [time, eventType] = std::move(*events.begin()); + events.erase(events.begin()); + + scheduler.updateNextTimestamp(); + + switch (eventType) { + case Scheduler::EventType::VBlank: [[likely]] { + // Signal that we've reached the end of a frame + frameDone = true; + lua.signalEvent(LuaEvent::Frame); + + // Send VBlank interrupts + 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; + + default: { + Helpers::panic("Scheduler: Unimplemented event type received: %d\n", static_cast(eventType)); + break; + } + } + } +} + +// Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc) +// Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in +// %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart. +// If the portable build setting is enabled, then those saves go in the executable directory instead +std::filesystem::path Emulator::getAppDataRoot() { + std::filesystem::path appDataPath; + +#ifdef __ANDROID__ + appDataPath = getAndroidAppPath(); +#else + char* appData; + if (!config.usePortableBuild) { + appData = SDL_GetPrefPath(nullptr, "Alber"); + appDataPath = std::filesystem::path(appData); + } else { + appData = SDL_GetBasePath(); + appDataPath = std::filesystem::path(appData) / "Emulator Files"; + } + SDL_free(appData); +#endif + + return appDataPath; +} + bool Emulator::loadROM(const std::filesystem::path& path) { // Reset the emulator if we've already loaded a ROM if (romType != ROMType::None) { @@ -127,26 +186,7 @@ bool Emulator::loadROM(const std::filesystem::path& path) { memory.loadedCXI = std::nullopt; memory.loaded3DSX = std::nullopt; - // Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc) - // Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in - // %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart. - // If the portable build setting is enabled, then those saves go in the executable directory instead - std::filesystem::path appDataPath; - - #ifdef __ANDROID__ - appDataPath = getAndroidAppPath(); - #else - char* appData; - if (!config.usePortableBuild) { - appData = SDL_GetPrefPath(nullptr, "Alber"); - appDataPath = std::filesystem::path(appData); - } else { - appData = SDL_GetBasePath(); - appDataPath = std::filesystem::path(appData) / "Emulator Files"; - } - SDL_free(appData); - #endif - + const std::filesystem::path appDataPath = getAppDataRoot(); const std::filesystem::path dataPath = appDataPath / path.filename().stem(); const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt"; IOFile::setAppDataDir(dataPath); diff --git a/src/hydra_core.cpp b/src/hydra_core.cpp index d67ffe2f..acbf30a8 100644 --- a/src/hydra_core.cpp +++ b/src/hydra_core.cpp @@ -134,25 +134,7 @@ void HydraCore::setPollInputCallback(void (*callback)()) { pollInputCallback = c void HydraCore::setCheckButtonCallback(s32 (*callback)(u32 player, hydra::ButtonType button)) { checkButtonCallback = callback; } u32 HydraCore::addCheat(const u8* data, u32 size) { - // Every 3DS cheat is a multiple of 64 bits == 8 bytes - if ((size % 8) != 0) { - return hydra::BAD_CHEAT; - } - - Cheats::Cheat cheat; - cheat.enabled = true; - cheat.type = Cheats::CheatType::ActionReplay; - - for (u32 i = 0; i < size; i += 8) { - auto read32 = [](const u8* ptr) { return (u32(ptr[3]) << 24) | (u32(ptr[2]) << 16) | (u32(ptr[1]) << 8) | u32(ptr[0]); }; - - // Data is passed to us in big endian so we bswap - u32 firstWord = Common::swap32(read32(data + i)); - u32 secondWord = Common::swap32(read32(data + i + 4)); - cheat.instructions.insert(cheat.instructions.end(), {firstWord, secondWord}); - } - - return emulator->getCheats().addCheat(cheat); + return emulator->getCheats().addCheat(data, size); }; void HydraCore::removeCheat(u32 id) { emulator->getCheats().removeCheat(id); } diff --git a/src/jni_driver.cpp b/src/jni_driver.cpp index 6eeb727a..d962f23e 100644 --- a/src/jni_driver.cpp +++ b/src/jni_driver.cpp @@ -35,6 +35,13 @@ JNIEnv* jniEnv() { extern "C" { +#define MAKE_SETTING(functionName, type, settingName) \ +AlberFunction(void, functionName) (JNIEnv* env, jobject obj, type value) { emulator->getConfig().settingName = value; } + +MAKE_SETTING(setShaderJitEnabled, jboolean, shaderJitEnabled) + +#undef MAKE_SETTING + AlberFunction(void, Setup)(JNIEnv* env, jobject obj) { env->GetJavaVM(&jvm); } AlberFunction(void, Pause)(JNIEnv* env, jobject obj) { emulator->pause(); } AlberFunction(void, Resume)(JNIEnv* env, jobject obj) { emulator->resume(); } diff --git a/src/lua.cpp b/src/lua.cpp index ec1287bd..09c63173 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -1,6 +1,12 @@ #ifdef PANDA3DS_ENABLE_LUA #include "lua_manager.hpp" +#ifndef __ANDROID__ +extern "C" { + #include "luv.h" +} +#endif + void LuaManager::initialize() { L = luaL_newstate(); // Open Lua @@ -9,10 +15,15 @@ void LuaManager::initialize() { initialized = false; return; } - luaL_openlibs(L); - initializeThunks(); +#ifndef __ANDROID__ + lua_pushstring(L, "luv"); + luaopen_luv(L); + lua_settable(L, LUA_GLOBALSINDEX); +#endif + + initializeThunks(); initialized = true; haveScript = false; } diff --git a/src/panda_qt/cheats_window.cpp b/src/panda_qt/cheats_window.cpp new file mode 100644 index 00000000..dbd251cc --- /dev/null +++ b/src/panda_qt/cheats_window.cpp @@ -0,0 +1,268 @@ +#include "panda_qt/cheats_window.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cheats.hpp" +#include "emulator.hpp" +#include "panda_qt/main_window.hpp" + +MainWindow* mainWindow = nullptr; + +struct CheatMetadata { + u32 handle = Cheats::badCheatHandle; + std::string name = "New cheat"; + std::string code; + bool enabled = true; +}; + +void dispatchToMainThread(std::function callback) { + QTimer* timer = new QTimer(); + timer->moveToThread(qApp->thread()); + timer->setSingleShot(true); + QObject::connect(timer, &QTimer::timeout, [=]() + { + callback(); + timer->deleteLater(); + }); + QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0)); +} + +class CheatEntryWidget : public QWidget { + public: + CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent); + + void Update() { + name->setText(metadata.name.c_str()); + enabled->setChecked(metadata.enabled); + update(); + } + + void Remove() { + emu->getCheats().removeCheat(metadata.handle); + cheatList->takeItem(cheatList->row(listItem)); + deleteLater(); + } + + const CheatMetadata& getMetadata() { return metadata; } + void setMetadata(const CheatMetadata& metadata) { this->metadata = metadata; } + + private: + void checkboxChanged(int state); + void editClicked(); + + Emulator* emu; + CheatMetadata metadata; + u32 handle; + QLabel* name; + QCheckBox* enabled; + QListWidget* cheatList; + QListWidgetItem* listItem; +}; + +class CheatEditDialog : public QDialog { + public: + CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry); + + void accepted(); + void rejected(); + + private: + Emulator* emu; + CheatEntryWidget& cheatEntry; + QTextEdit* codeEdit; + QLineEdit* nameEdit; +}; + +CheatEntryWidget::CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent) + : QWidget(), emu(emu), metadata(metadata), cheatList(parent) { + QHBoxLayout* layout = new QHBoxLayout; + + enabled = new QCheckBox; + enabled->setChecked(metadata.enabled); + + name = new QLabel(metadata.name.c_str()); + QPushButton* buttonEdit = new QPushButton(tr("Edit")); + + connect(enabled, &QCheckBox::stateChanged, this, &CheatEntryWidget::checkboxChanged); + connect(buttonEdit, &QPushButton::clicked, this, &CheatEntryWidget::editClicked); + + layout->addWidget(enabled); + layout->addWidget(name); + layout->addWidget(buttonEdit); + setLayout(layout); + + listItem = new QListWidgetItem; + listItem->setSizeHint(sizeHint()); + parent->addItem(listItem); + parent->setItemWidget(listItem, this); +} + +void CheatEntryWidget::checkboxChanged(int state) { + bool enabled = state == Qt::Checked; + if (metadata.handle == Cheats::badCheatHandle) { + printf("Cheat handle is bad, this shouldn't happen\n"); + return; + } + + if (enabled) { + emu->getCheats().enableCheat(metadata.handle); + metadata.enabled = true; + } else { + emu->getCheats().disableCheat(metadata.handle); + metadata.enabled = false; + } +} + +void CheatEntryWidget::editClicked() { + CheatEditDialog* dialog = new CheatEditDialog(emu, *this); + dialog->show(); +} + +CheatEditDialog::CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry) : QDialog(), emu(emu), cheatEntry(cheatEntry) { + setAttribute(Qt::WA_DeleteOnClose); + setModal(true); + + QVBoxLayout* layout = new QVBoxLayout; + const CheatMetadata& metadata = cheatEntry.getMetadata(); + codeEdit = new QTextEdit; + nameEdit = new QLineEdit; + nameEdit->setText(metadata.name.c_str()); + nameEdit->setPlaceholderText(tr("Cheat name")); + layout->addWidget(nameEdit); + + QFont font; + font.setFamily("Courier"); + font.setFixedPitch(true); + font.setPointSize(10); + codeEdit->setFont(font); + + if (metadata.code.size() != 0) { + // Nicely format it like so: + // 01234567 89ABCDEF + // 01234567 89ABCDEF + std::string formattedCode; + for (size_t i = 0; i < metadata.code.size(); i += 2) { + if (i != 0) { + if (i % 8 == 0 && i % 16 != 0) { + formattedCode += " "; + } else if (i % 16 == 0) { + formattedCode += "\n"; + } + } + + formattedCode += metadata.code[i]; + formattedCode += metadata.code[i + 1]; + } + codeEdit->setText(formattedCode.c_str()); + } + + layout->addWidget(codeEdit); + setLayout(layout); + + auto buttons = QDialogButtonBox::Ok | QDialogButtonBox::Cancel; + QDialogButtonBox* buttonBox = new QDialogButtonBox(buttons); + layout->addWidget(buttonBox); + + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(this, &QDialog::rejected, this, &CheatEditDialog::rejected); + connect(this, &QDialog::accepted, this, &CheatEditDialog::accepted); +} + +void CheatEditDialog::accepted() { + QString code = codeEdit->toPlainText(); + code.replace(QRegularExpression("[^0-9a-fA-F]"), ""); + + CheatMetadata metadata = cheatEntry.getMetadata(); + metadata.name = nameEdit->text().toStdString(); + metadata.code = code.toStdString(); + cheatEntry.setMetadata(metadata); + + std::vector bytes; + for (size_t i = 0; i < metadata.code.size(); i += 2) { + std::string hex = metadata.code.substr(i, 2); + bytes.push_back((u8)std::stoul(hex, nullptr, 16)); + } + + mainWindow->editCheat(cheatEntry.getMetadata().handle, bytes, [this](u32 handle) { + dispatchToMainThread([this, handle]() { + if (handle == Cheats::badCheatHandle) { + cheatEntry.Remove(); + return; + } else { + CheatMetadata metadata = cheatEntry.getMetadata(); + metadata.handle = handle; + cheatEntry.setMetadata(metadata); + cheatEntry.Update(); + } + }); + }); +} + +void CheatEditDialog::rejected() { + bool isEditing = cheatEntry.getMetadata().handle != Cheats::badCheatHandle; + if (!isEditing) { + // Was adding a cheat but user pressed cancel + cheatEntry.Remove(); + } +} + +CheatsWindow::CheatsWindow(Emulator* emu, const std::filesystem::path& cheatPath, QWidget* parent) + : QWidget(parent, Qt::Window), emu(emu), cheatPath(cheatPath) { + mainWindow = static_cast(parent); + + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(6, 6, 6, 6); + setLayout(layout); + + cheatList = new QListWidget; + layout->addWidget(cheatList); + + QWidget* buttonBox = new QWidget; + QHBoxLayout* buttonLayout = new QHBoxLayout; + + QPushButton* buttonAdd = new QPushButton(tr("Add")); + QPushButton* buttonRemove = new QPushButton(tr("Remove")); + + connect(buttonAdd, &QPushButton::clicked, this, &CheatsWindow::addEntry); + connect(buttonRemove, &QPushButton::clicked, this, &CheatsWindow::removeClicked); + + buttonLayout->addWidget(buttonAdd); + buttonLayout->addWidget(buttonRemove); + buttonBox->setLayout(buttonLayout); + + layout->addWidget(buttonBox); + + // TODO: load cheats from saved cheats per game + // for (const CheatMetadata& metadata : getSavedCheats()) + // { + // new CheatEntryWidget(emu, metadata, cheatList); + // } +} + +void CheatsWindow::addEntry() { + // CheatEntryWidget is added to the list when it's created + CheatEntryWidget* entry = new CheatEntryWidget(emu, {Cheats::badCheatHandle, "New cheat", "", true}, cheatList); + CheatEditDialog* dialog = new CheatEditDialog(emu, *entry); + dialog->show(); +} + +void CheatsWindow::removeClicked() { + QListWidgetItem* item = cheatList->currentItem(); + if (item == nullptr) { + return; + } + + CheatEntryWidget* entry = static_cast(cheatList->itemWidget(item)); + entry->Remove(); +} diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index e390aa44..de70cc18 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -1,9 +1,14 @@ #include "panda_qt/main_window.hpp" +#include #include +#include +#include #include #include +#include "cheats.hpp" + MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), screen(this) { setWindowTitle("Alber"); // Enable drop events for loading ROMs @@ -26,8 +31,14 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) // Create and bind actions for them auto loadGameAction = fileMenu->addAction(tr("Load game")); auto loadLuaAction = fileMenu->addAction(tr("Load Lua script")); + auto openAppFolderAction = fileMenu->addAction(tr("Open Panda3DS folder")); + connect(loadGameAction, &QAction::triggered, this, &MainWindow::selectROM); connect(loadLuaAction, &QAction::triggered, this, &MainWindow::selectLuaFile); + connect(openAppFolderAction, &QAction::triggered, this, [this]() { + QString path = QString::fromStdU16String(emu->getAppDataRoot().u16string()); + QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); auto pauseAction = emulationMenu->addAction(tr("Pause")); auto resumeAction = emulationMenu->addAction(tr("Resume")); @@ -40,20 +51,23 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); + auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); + connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor); auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu); + emu = new Emulator(); + emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight); + // Set up misc objects aboutWindow = new AboutWindow(nullptr); configWindow = new ConfigWindow(this); + cheatsEditor = new CheatsWindow(emu, {}, this); luaEditor = new TextEditorWindow(this, "script.lua", ""); - emu = new Emulator(); - emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight); - auto args = QCoreApplication::arguments(); if (args.size() > 1) { auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String(); @@ -176,6 +190,7 @@ MainWindow::~MainWindow() { delete menuBar; delete aboutWindow; delete configWindow; + delete cheatsEditor; delete luaEditor; } @@ -194,8 +209,7 @@ void MainWindow::dumpRomFS() { return; } std::filesystem::path path(folder.toStdU16String()); - - // TODO: This might break if the game accesses RomFS while we're dumping, we should move it to the emulator thread when we've got a message queue going + messageQueueMutex.lock(); RomFS::DumpingResult res = emu->dumpRomFS(path); messageQueueMutex.unlock(); @@ -226,6 +240,7 @@ void MainWindow::showAboutMenu() { } void MainWindow::openLuaEditor() { luaEditor->show(); } +void MainWindow::openCheatsEditor() { cheatsEditor->show(); } void MainWindow::dispatchMessage(const EmulatorMessage& message) { switch (message.type) { @@ -240,12 +255,33 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { delete message.string.str; break; + case MessageType::EditCheat: { + u32 handle = message.cheat.c->handle; + const std::vector& cheat = message.cheat.c->cheat; + const std::function& callback = message.cheat.c->callback; + bool isEditing = handle != Cheats::badCheatHandle; + if (isEditing) { + emu->getCheats().removeCheat(handle); + u32 handle = emu->getCheats().addCheat(cheat.data(), cheat.size()); + } else { + u32 handle = emu->getCheats().addCheat(cheat.data(), cheat.size()); + callback(handle); + } + delete message.cheat.c; + } break; + case MessageType::Pause: emu->pause(); break; case MessageType::Resume: emu->resume(); break; case MessageType::TogglePause: emu->togglePause(); break; case MessageType::Reset: emu->reset(Emulator::ReloadOption::Reload); break; case MessageType::PressKey: emu->getServiceManager().getHID().pressKey(message.key.key); break; case MessageType::ReleaseKey: emu->getServiceManager().getHID().releaseKey(message.key.key); break; + case MessageType::SetCirclePadX: emu->getServiceManager().getHID().setCirclepadX(message.circlepad.value); break; + case MessageType::SetCirclePadY: emu->getServiceManager().getHID().setCirclepadY(message.circlepad.value); break; + case MessageType::PressTouchscreen: + emu->getServiceManager().getHID().setTouchScreenPress(message.touchscreen.x, message.touchscreen.y); + break; + case MessageType::ReleaseTouchscreen: emu->getServiceManager().getHID().releaseTouchScreen(); break; } } @@ -253,7 +289,12 @@ void MainWindow::keyPressEvent(QKeyEvent* event) { auto pressKey = [this](u32 key) { EmulatorMessage message{.type = MessageType::PressKey}; message.key.key = key; + sendMessage(message); + }; + auto setCirclePad = [this](MessageType type, s16 value) { + EmulatorMessage message{.type = type}; + message.circlepad.value = value; sendMessage(message); }; @@ -266,6 +307,11 @@ void MainWindow::keyPressEvent(QKeyEvent* event) { case Qt::Key_Q: pressKey(HID::Keys::L); break; case Qt::Key_P: pressKey(HID::Keys::R); break; + case Qt::Key_W: setCirclePad(MessageType::SetCirclePadY, 0x9C); break; + case Qt::Key_A: setCirclePad(MessageType::SetCirclePadX, -0x9C); break; + case Qt::Key_S: setCirclePad(MessageType::SetCirclePadY, -0x9C); break; + case Qt::Key_D: setCirclePad(MessageType::SetCirclePadX, 0x9C); break; + case Qt::Key_Right: pressKey(HID::Keys::Right); break; case Qt::Key_Left: pressKey(HID::Keys::Left); break; case Qt::Key_Up: pressKey(HID::Keys::Up); break; @@ -282,7 +328,12 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) { auto releaseKey = [this](u32 key) { EmulatorMessage message{.type = MessageType::ReleaseKey}; message.key.key = key; + sendMessage(message); + }; + auto releaseCirclePad = [this](MessageType type) { + EmulatorMessage message{.type = type}; + message.circlepad.value = 0; sendMessage(message); }; @@ -295,6 +346,12 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) { case Qt::Key_Q: releaseKey(HID::Keys::L); break; case Qt::Key_P: releaseKey(HID::Keys::R); break; + case Qt::Key_W: + case Qt::Key_S: releaseCirclePad(MessageType::SetCirclePadY); break; + + case Qt::Key_A: + case Qt::Key_D: releaseCirclePad(MessageType::SetCirclePadX); break; + case Qt::Key_Right: releaseKey(HID::Keys::Right); break; case Qt::Key_Left: releaseKey(HID::Keys::Left); break; case Qt::Key_Up: releaseKey(HID::Keys::Up); break; @@ -305,10 +362,56 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) { } } +void MainWindow::mousePressEvent(QMouseEvent* event) { + if (event->button() == Qt::MouseButton::LeftButton) { + const QPointF clickPos = event->globalPosition(); + const QPointF widgetPos = screen.mapFromGlobal(clickPos); + + // Press is inside the screen area + if (widgetPos.x() >= 0 && widgetPos.x() < screen.width() && widgetPos.y() >= 0 && widgetPos.y() < screen.height()) { + // Go from widget positions to [0, 400) for x and [0, 480) for y + uint x = (uint)std::round(widgetPos.x() / screen.width() * 400.f); + uint y = (uint)std::round(widgetPos.y() / screen.height() * 480.f); + + // Check if touch falls in the touch screen area + if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { + // Convert to 3DS coordinates + u16 x_converted = static_cast(x) - 40; + u16 y_converted = static_cast(y) - 240; + + EmulatorMessage message{.type = MessageType::PressTouchscreen}; + message.touchscreen.x = x_converted; + message.touchscreen.y = y_converted; + sendMessage(message); + } else { + sendMessage(EmulatorMessage{.type = MessageType::ReleaseTouchscreen}); + } + } + } +} + +void MainWindow::mouseReleaseEvent(QMouseEvent* event) { + if (event->button() == Qt::MouseButton::LeftButton) { + sendMessage(EmulatorMessage{.type = MessageType::ReleaseTouchscreen}); + } +} + void MainWindow::loadLuaScript(const std::string& code) { EmulatorMessage message{.type = MessageType::LoadLuaScript}; // Make a copy of the code on the heap to send via the message queue message.string.str = new std::string(code); sendMessage(message); +} + +void MainWindow::editCheat(u32 handle, const std::vector& cheat, const std::function& callback) { + EmulatorMessage message{.type = MessageType::EditCheat}; + + CheatMessage* c = new CheatMessage(); + c->handle = handle; + c->cheat = cheat; + c->callback = callback; + + message.cheat.c = c; + sendMessage(message); } \ No newline at end of file diff --git a/src/panda_qt/text_editor.cpp b/src/panda_qt/text_editor.cpp index c189c2ce..a31a829f 100644 --- a/src/panda_qt/text_editor.cpp +++ b/src/panda_qt/text_editor.cpp @@ -16,8 +16,8 @@ TextEditorWindow::TextEditorWindow(QWidget* parent, const std::string& filename, ZepReplExCommand::Register(zepWidget.GetEditor(), &replProvider); // Default to standard mode instead of vim mode, initialize text box - zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName()); zepWidget.GetEditor().InitWithText(filename, initialText); + zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName()); // Layout for widgets QVBoxLayout* mainLayout = new QVBoxLayout(); @@ -41,4 +41,4 @@ TextEditorWindow::TextEditorWindow(QWidget* parent, const std::string& filename, mainLayout->addWidget(button); mainLayout->addWidget(&zepWidget); -} \ No newline at end of file +} diff --git a/src/pandroid/app/build.gradle.kts b/src/pandroid/app/build.gradle.kts index f1feaf0d..201d5db1 100644 --- a/src/pandroid/app/build.gradle.kts +++ b/src/pandroid/app/build.gradle.kts @@ -21,8 +21,20 @@ android { } buildTypes { - release { + getByName("release") { isMinifyEnabled = false + isShrinkResources = false + isDebuggable = false + signingConfig = signingConfigs.getByName("debug") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + getByName("debug") { + isMinifyEnabled = false + isShrinkResources = false + isDebuggable = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -41,4 +53,4 @@ dependencies { implementation("androidx.preference:preference:1.2.1") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("com.google.code.gson:gson:2.10.1") -} \ No newline at end of file +} diff --git a/src/pandroid/app/src/main/AndroidManifest.xml b/src/pandroid/app/src/main/AndroidManifest.xml index 9f767654..c66d37af 100644 --- a/src/pandroid/app/src/main/AndroidManifest.xml +++ b/src/pandroid/app/src/main/AndroidManifest.xml @@ -20,6 +20,8 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:isGame="true" + android:hardwareAccelerated="true" android:theme="@style/Theme.Pandroid" tools:targetApi="31"> + + diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java index 5cff703c..00b7842b 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/AlberDriver.java @@ -22,5 +22,7 @@ public class AlberDriver { public static native void LoadLuaScript(String script); public static native byte[] GetSmdh(); + public static native void setShaderJitEnabled(boolean enable); + static { System.loadLibrary("Alber"); } } \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java index aced6faa..f7050e99 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/GameActivity.java @@ -21,6 +21,7 @@ import com.panda3ds.pandroid.input.InputMap; import com.panda3ds.pandroid.utils.Constants; import com.panda3ds.pandroid.view.PandaGlSurfaceView; import com.panda3ds.pandroid.view.PandaLayoutController; +import com.panda3ds.pandroid.view.utils.PerformanceView; public class GameActivity extends BaseActivity { private final DrawerFragment drawerFragment = new DrawerFragment(); @@ -56,6 +57,11 @@ public class GameActivity extends BaseActivity { ((CheckBox) findViewById(R.id.hide_screen_controller)).setChecked(GlobalConfig.get(GlobalConfig.KEY_SCREEN_GAMEPAD_VISIBLE)); getSupportFragmentManager().beginTransaction().replace(R.id.drawer_fragment, drawerFragment).commitNow(); + + if (GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)) { + PerformanceView view = new PerformanceView(this); + ((FrameLayout) findViewById(R.id.panda_gl_frame)).addView(view, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } } @Override diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java index 02fbbbcc..b0cdc935 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PandroidApplication.java @@ -2,11 +2,13 @@ package com.panda3ds.pandroid.app; import android.app.Application; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import com.panda3ds.pandroid.AlberDriver; import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.services.LoggerService; import com.panda3ds.pandroid.data.config.GlobalConfig; import com.panda3ds.pandroid.input.InputMap; import com.panda3ds.pandroid.utils.GameUtils; @@ -24,6 +26,10 @@ public class PandroidApplication extends Application { GameUtils.initialize(); InputMap.initialize(); AlberDriver.Setup(); + + if (GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE)) { + startService(new Intent(this, LoggerService.class)); + } } public static int getThemeId() { 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 3cd28f4b..9482df1d 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 @@ -1,8 +1,13 @@ package com.panda3ds.pandroid.app.base; import android.annotation.SuppressLint; + +import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatActivity; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; + import com.panda3ds.pandroid.lang.Function; @@ -15,4 +20,8 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { return false; }); } + + protected void setActivityTitle(@StringRes int titleId) { + ((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(titleId); + } } 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 b3bebf8f..bfe33a2b 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,6 +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.InputPreferences; public class SettingsFragment extends BasePreferenceFragment { @@ -16,5 +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)); } } 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/DeveloperPreferences.java new file mode 100644 index 00000000..f131f0a0 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/DeveloperPreferences.java @@ -0,0 +1,49 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.preference.SwitchPreference; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PandroidApplication; +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 { + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.developer_preferences, rootKey); + setActivityTitle(R.string.developer_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())); + setItemClick("loggerService", pref -> { + boolean checked = ((SwitchPreference) pref).isChecked(); + Context ctx = PandroidApplication.getAppContext(); + if (checked) { + ctx.startService(new Intent(ctx, LoggerService.class)); + } else { + ctx.stopService(new Intent(ctx, LoggerService.class)); + } + GlobalConfig.set(GlobalConfig.KEY_LOGGER_SERVICE, checked); + }); + + refresh(); + } + + @Override + public void onResume() { + super.onResume(); + refresh(); + } + + private void refresh() { + ((SwitchPreference) findPreference("performanceMonitor")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)); + ((SwitchPreference) findPreference("loggerService")).setChecked(GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE)); + ((SwitchPreference) findPreference("shaderJit")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT)); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/services/LoggerService.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/services/LoggerService.java new file mode 100644 index 00000000..e44f3503 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/services/LoggerService.java @@ -0,0 +1,104 @@ +package com.panda3ds.pandroid.app.services; + +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.lang.PipeStreamTask; +import com.panda3ds.pandroid.lang.Task; +import com.panda3ds.pandroid.utils.Constants; +import com.panda3ds.pandroid.utils.FileUtils; + +import java.io.OutputStream; +import java.util.Arrays; + +public class LoggerService extends Service { + private static final long MAX_LOG_SIZE = 1024 * 1024 * 4; // 4MB + + private PipeStreamTask errorTask; + private PipeStreamTask outputTask; + private Process logcat; + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + try { + Runtime.getRuntime().exec(new String[]{"logcat", "-c"}).waitFor(); + logcat = Runtime.getRuntime().exec(new String[]{"logcat"}); + String logPath = getExternalMediaDirs()[0].getAbsolutePath(); + FileUtils.createDir(logPath, "logs"); + logPath = logPath + "/logs"; + + if (FileUtils.exists(logPath + "/last.txt")) { + FileUtils.delete(logPath + "/last.txt"); + } + + if (FileUtils.exists(logPath + "/current.txt")) { + FileUtils.rename(logPath + "/current.txt", "last.txt"); + } + + OutputStream stream = FileUtils.getOutputStream(logPath + "/current.txt"); + errorTask = new PipeStreamTask(logcat.getErrorStream(), stream, MAX_LOG_SIZE); + outputTask = new PipeStreamTask(logcat.getInputStream(), stream, MAX_LOG_SIZE); + + errorTask.start(); + outputTask.start(); + + Log.i(Constants.LOG_TAG, "Started logger service"); + logDeviceInfo(); + } catch (Exception e) { + stopSelf(); + Log.e(Constants.LOG_TAG, "Failed to start logger service"); + } + } + + private void logDeviceInfo() { + Log.i(Constants.LOG_TAG, "----------------------"); + Log.i(Constants.LOG_TAG, "Android SDK: " + Build.VERSION.SDK_INT); + Log.i(Constants.LOG_TAG, "Device: " + Build.DEVICE); + Log.i(Constants.LOG_TAG, "Model: " + Build.MANUFACTURER + " " + Build.MODEL); + Log.i(Constants.LOG_TAG, "ABIs: " + Arrays.toString(Build.SUPPORTED_ABIS)); + try { + PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0); + Log.i(Constants.LOG_TAG, ""); + Log.i(Constants.LOG_TAG, "Package: " + info.packageName); + Log.i(Constants.LOG_TAG, "Install location: " + info.installLocation); + Log.i(Constants.LOG_TAG, "App version: " + info.versionName + " (" + info.versionCode + ")"); + } catch (Exception e) { + Log.e(Constants.LOG_TAG, "Error obtaining package info: " + e); + } + Log.i(Constants.LOG_TAG, "----------------------"); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + stopSelf(); + //This is a time for app save save log file + try { + Thread.sleep(1000); + } catch (Exception e) {} + super.onTaskRemoved(rootIntent); + } + + @Override + public void onDestroy() { + Log.i(Constants.LOG_TAG, "Logger service terminating"); + errorTask.close(); + outputTask.close(); + try { + logcat.destroy(); + } catch (Throwable t) {} + super.onDestroy(); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java index d6dbe3b8..bff1f9e0 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java @@ -5,7 +5,6 @@ import com.panda3ds.pandroid.data.GsonConfigParser; import com.panda3ds.pandroid.utils.Constants; import java.io.Serializable; -import java.util.HashMap; import java.util.Map; public class GlobalConfig { @@ -19,6 +18,9 @@ public class GlobalConfig { public static DataModel data; + public static final Key KEY_SHADER_JIT = new Key<>("emu.shader_jit", false); + public static final Key KEY_SHOW_PERFORMANCE_OVERLAY = new Key<>("dev.performanceOverlay", false); + public static final Key KEY_LOGGER_SERVICE = new Key<>("dev.loggerService", false); public static final Key KEY_APP_THEME = new Key<>("app.theme", THEME_ANDROID); public static final Key KEY_SCREEN_GAMEPAD_VISIBLE = new Key<>("app.screen_gamepad.visible", true); diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/PipeStreamTask.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/PipeStreamTask.java new file mode 100644 index 00000000..e4bbda98 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/PipeStreamTask.java @@ -0,0 +1,40 @@ +package com.panda3ds.pandroid.lang; + +import java.io.InputStream; +import java.io.OutputStream; + +public class PipeStreamTask extends Task { + private final InputStream input; + private final OutputStream output; + private final long limit; + private long size; + + public PipeStreamTask(InputStream input, OutputStream output, long limit) { + this.input = input; + this.output = output; + this.limit = limit; + } + + @Override + public void run() { + super.run(); + int data; + try { + while ((data = input.read()) != -1) { + output.write(data); + if (++size > limit) { + break; + } + } + } catch (Exception e) {} + close(); + } + + public void close() { + try { + output.flush(); + output.close(); + input.close(); + } catch (Exception e) {} + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Task.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Task.java index 7745883d..8de344b4 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Task.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/lang/Task.java @@ -5,6 +5,8 @@ public class Task extends Thread { super(runnable); } + protected Task() {} + public void runSync() { start(); waitFinish(); 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 45faf5a4..1746f1c9 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 @@ -70,6 +70,25 @@ public class FileUtils { return parseFile(path).exists(); } + public static void rename(String path, String newName){ + parseFile(path).renameTo(newName); + } + + public static void delete(String path) { + DocumentFile file = parseFile(path); + + if (file.exists()) { + if (file.isDirectory()) { + String[] children = listFiles(path); + for (String child : children) { + delete(path + "/" + child); + } + } + + file.delete(); + } + } + public static boolean createDir(String path, String name) { DocumentFile folder = parseFile(path); if (folder.findFile(name) != null) { diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PerformanceMonitor.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PerformanceMonitor.java new file mode 100644 index 00000000..23adbf13 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/PerformanceMonitor.java @@ -0,0 +1,64 @@ +package com.panda3ds.pandroid.utils; + +import android.app.ActivityManager; +import android.content.Context; +import android.os.Debug; +import android.os.Process; + +import com.panda3ds.pandroid.app.PandroidApplication; +import com.panda3ds.pandroid.data.config.GlobalConfig; + +public class PerformanceMonitor { + private static int fps = 1; + private static String backend = ""; + private static int frames = 0; + private static long lastUpdate = 0; + private static long totalMemory = 1; + private static long availableMemory = 0; + + public static void initialize(String backendName) { + fps = 1; + backend = backendName; + } + + public static void runFrame() { + if (GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)) { + frames++; + if (System.currentTimeMillis() - lastUpdate > 1000) { + lastUpdate = System.currentTimeMillis(); + fps = frames; + frames = 0; + try { + Context ctx = PandroidApplication.getAppContext(); + ActivityManager manager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo(); + manager.getMemoryInfo(info); + totalMemory = info.totalMem; + availableMemory = info.availMem; + } catch (Exception e) {} + } + } + } + + public static long getUsedMemory() { + return Math.max(1, totalMemory - availableMemory); + } + + public static long getTotalMemory() { + return totalMemory; + } + + public static long getAvailableMemory() { + return availableMemory; + } + + public static int getFps() { + return fps; + } + + public static String getBackend() { + return backend; + } + + public static void destroy() {} +} 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 52e609a3..c39b36b3 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 @@ -7,8 +7,10 @@ import android.graphics.Rect; import android.opengl.GLSurfaceView; import android.util.Log; import com.panda3ds.pandroid.AlberDriver; +import com.panda3ds.pandroid.data.config.GlobalConfig; 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; @@ -38,9 +40,12 @@ 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); } + + PerformanceMonitor.destroy(); super.finalize(); } @@ -78,6 +83,7 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer glBindFramebuffer(GL_FRAMEBUFFER, 0); AlberDriver.Initialize(); + AlberDriver.setShaderJitEnabled(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT)); AlberDriver.LoadRom(romPath); // Load the SMDH @@ -92,6 +98,8 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer GameUtils.removeGame(game); GameUtils.addGame(GameMetadata.applySMDH(game, smdh)); } + + PerformanceMonitor.initialize(getBackendName()); } public void onDrawFrame(GL10 unused) { @@ -114,6 +122,8 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer screenHeight - bottomScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR ); } + + PerformanceMonitor.runFrame(); } public void onSurfaceChanged(GL10 unused, int width, int height) { 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 new file mode 100644 index 00000000..e4d7be15 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/utils/PerformanceView.java @@ -0,0 +1,64 @@ +package com.panda3ds.pandroid.view.utils; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.text.Html; +import android.util.AttributeSet; +import android.util.TypedValue; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +import com.panda3ds.pandroid.data.config.GlobalConfig; +import com.panda3ds.pandroid.utils.PerformanceMonitor; + +public class PerformanceView extends AppCompatTextView { + private boolean running = false; + + public PerformanceView(@NonNull Context context) { + this(context, null); + } + + public PerformanceView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs,0); + } + + public PerformanceView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()); + setPadding(padding,padding,padding,padding); + setTextColor(Color.WHITE); + setShadowLayer(padding,0,0,Color.BLACK); + } + + public void refresh(){ + running = isShown(); + if (!running) { + return; + } + + String debug = ""; + + // Calculate total memory in MB and the current memory usage + int memoryTotalMb = (int) Math.round(PerformanceMonitor.getTotalMemory() / (1024.0 * 1024.0)); + int memoryUsageMb = (int) Math.round(PerformanceMonitor.getUsedMemory() / (1024.0 * 1024.0)); + + debug += "FPS: " + PerformanceMonitor.getFps() + "
"; + debug += "RAM: " + Math.round(((float) memoryUsageMb / memoryTotalMb) * 100) + "% (" + memoryUsageMb + "MB/" + memoryTotalMb + "MB)
"; + debug += "BACKEND: " + PerformanceMonitor.getBackend() + (GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT) ? " + JIT" : "") + "
"; + setText(Html.fromHtml(debug, Html.FROM_HTML_MODE_COMPACT)); + postDelayed(this::refresh, 250); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (!running) { + refresh(); + } + } +} 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 065b9e4f..1198d66b 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,4 +45,13 @@ Abrir arquivo Criar novo Executando \"%s\" ... - \ No newline at end of file + Opções de desenvolvedor + Depuração, mostrar fps, etc. + Monitor de desempenho + Mostrar um overlay com fps, memoria, etc. + Depuração + Grave os registros para um arquivo. + Shader Jit + Usar recompilador de shaders. + Gráficos + diff --git a/src/pandroid/app/src/main/res/values/strings.xml b/src/pandroid/app/src/main/res/values/strings.xml index e0de62e1..4c64439c 100644 --- a/src/pandroid/app/src/main/res/values/strings.xml +++ b/src/pandroid/app/src/main/res/values/strings.xml @@ -46,4 +46,13 @@ Open file Create new Running \"%s\" ... + Developer options + Logger, FPS Counter, etc. + Performance monitor + Show overlay with fps, memory, etc. + Logger + Store application logs to file. + Shader JIT + Use shader recompiler. + Graphics diff --git a/src/pandroid/app/src/main/res/values/themes.xml b/src/pandroid/app/src/main/res/values/themes.xml index 2ade7102..343fab28 100644 --- a/src/pandroid/app/src/main/res/values/themes.xml +++ b/src/pandroid/app/src/main/res/values/themes.xml @@ -24,7 +24,11 @@ 32sp - - \ No newline at end of file + diff --git a/src/pandroid/app/src/main/res/xml/developer_preferences.xml b/src/pandroid/app/src/main/res/xml/developer_preferences.xml new file mode 100644 index 00000000..96ce8906 --- /dev/null +++ b/src/pandroid/app/src/main/res/xml/developer_preferences.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file 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 878aa920..5eeb1954 100644 --- a/src/pandroid/app/src/main/res/xml/start_preferences.xml +++ b/src/pandroid/app/src/main/res/xml/start_preferences.xml @@ -23,4 +23,11 @@ app:summary="@string/pref_appearance_summary" app:layout="@layout/preference_start_item"/> + + \ No newline at end of file diff --git a/third_party/libuv b/third_party/libuv new file mode 160000 index 00000000..b8368a14 --- /dev/null +++ b/third_party/libuv @@ -0,0 +1 @@ +Subproject commit b8368a1441fd4ebdaaae70b67136c80b1a98be32 diff --git a/third_party/luv b/third_party/luv new file mode 160000 index 00000000..3e55ac43 --- /dev/null +++ b/third_party/luv @@ -0,0 +1 @@ +Subproject commit 3e55ac4331d06aa5f43016a142aa2aaa23264105