diff --git a/.github/workflows/MacOS_Build.yml b/.github/workflows/MacOS_Build.yml index f6fafde99..912c85688 100644 --- a/.github/workflows/MacOS_Build.yml +++ b/.github/workflows/MacOS_Build.yml @@ -40,7 +40,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Install bundle dependencies - run: brew install dylibbundler imagemagick + run: brew install --overwrite python@3.12 && brew install dylibbundler imagemagick - name: Run bundle script run: ./.github/mac-bundle.sh diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 4d5c8b57c..40141fb16 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -67,7 +67,7 @@ jobs: - name: Install bundle dependencies run: | - brew install dylibbundler imagemagick + brew install --overwrite python@3.12 && brew install dylibbundler imagemagick - name: Install qt run: brew install qt && which macdeployqt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..740ccf0cc --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,130 @@ +# DESCRIPTION: GitLab CI/CD for libRetro (NOT FOR GitLab-proper) + +############################################################################## +################################# BOILERPLATE ################################ +############################################################################## + +# Core definitions +.core-defs: + variables: + GIT_SUBMODULE_STRATEGY: recursive + CORENAME: panda3ds + CORE_ARGS: -DBUILD_LIBRETRO_CORE=ON -DENABLE_USER_BUILD=ON -DENABLE_VULKAN=OFF -DENABLE_LUAJIT=OFF -DENABLE_DISCORD_RPC=OFF + +# Inclusion templates, required for the build to work + +include: + ################################## DESKTOPS ################################ + # Linux + - project: 'libretro-infrastructure/ci-templates' + file: '/linux-cmake.yml' + + # Windows + - project: 'libretro-infrastructure/ci-templates' + file: '/windows-cmake-mingw.yml' + + # MacOS + - project: 'libretro-infrastructure/ci-templates' + file: 'osx-cmake-x86.yml' + + # MacOS + - project: 'libretro-infrastructure/ci-templates' + file: 'osx-cmake-arm64.yml' + + ################################## CELLULAR ################################ + # Android + - project: 'libretro-infrastructure/ci-templates' + file: '/android-cmake.yml' + + # iOS + - project: 'libretro-infrastructure/ci-templates' + file: '/ios-cmake.yml' + +# Stages for building +stages: + - build-prepare + - build-static + - build-shared + +############################################################################## +#################################### STAGES ################################## +############################################################################## +# +################################### DESKTOPS ################################# +# Linux 64-bit +libretro-build-linux-x64: + image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-amd64-ubuntu:latest + before_script: + - export NUMPROC=$(($(nproc)/5)) + - sudo apt-get update -qy + - sudo apt-get install -qy software-properties-common + - sudo add-apt-repository -y ppa:savoury1/build-tools + - sudo add-apt-repository -y ppa:savoury1/gcc-defaults-12 + - sudo apt-get update -qy + - sudo apt-get install -qy cmake gcc-12 g++-12 + variables: + CC: /usr/bin/gcc-12 + CXX: /usr/bin/g++-12 + extends: + - .libretro-linux-cmake-x86_64 + - .core-defs + +# Windows 64-bit +libretro-build-windows-x64: + extends: + - .libretro-windows-cmake-x86_64 + - .core-defs + +# MacOS 64-bit +libretro-build-osx-x64: + tags: + - mac-apple-silicon + variables: + CORE_ARGS: -DBUILD_LIBRETRO_CORE=ON -DENABLE_USER_BUILD=ON -DENABLE_VULKAN=OFF -DENABLE_LUAJIT=OFF -DENABLE_DISCORD_RPC=OFF -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCRYPTOPP_AMD64=1 + extends: + - .libretro-osx-cmake-x86 + - .core-defs + +# MacOS arm 64-bit +libretro-build-osx-arm64: + tags: + - mac-apple-silicon + extends: + - .libretro-osx-cmake-arm64 + - .core-defs + +################################### CELLULAR ################################# +# Android ARMv7a +#android-armeabi-v7a: +# extends: +# - .libretro-android-cmake-armeabi-v7a +# - .core-defs + +# Android ARMv8a +# android-arm64-v8a: +# extends: +# - .libretro-android-cmake-arm64-v8a +# - .core-defs + +# Android 64-bit x86 +# android-x86_64: +# extends: +# - .libretro-android-cmake-x86_64 +# - .core-defs + +# Android 32-bit x86 +# android-x86: +# extends: +# - .libretro-android-cmake-x86 +# - .core-defs + +# iOS +# libretro-build-ios-arm64: +# extends: +# - .libretro-ios-cmake-arm64 +# - .core-defs +# variables: +# CORE_ARGS: -DBUILD_LIBRETRO_CORE=ON -DBUILD_PLAY=OFF -DENABLE_AMAZON_S3=off -DBUILD_TESTS=OFF -DCMAKE_TOOLCHAIN_FILE=deps/Dependencies/cmake-ios/ios.cmake -DTARGET_IOS=ON +# LIBNAME: ${CORENAME}_libretro_ios.dylib + +################################### CONSOLES ################################# diff --git a/CMakeLists.txt b/CMakeLists.txt index b6abd5310..5317c63a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -261,7 +261,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp - include/PICA/pica_vert_config.hpp + include/PICA/pica_vert_config.hpp include/sdl_sensors.hpp ) cmrc_add_resource_library( @@ -521,17 +521,17 @@ elseif(BUILD_HYDRA_CORE) target_link_libraries(Alber PUBLIC AlberCore) elseif(BUILD_LIBRETRO_CORE) include_directories(third_party/libretro/include) - add_library(Alber SHARED src/libretro_core.cpp) - target_link_libraries(Alber PUBLIC AlberCore) - - set_target_properties(Alber PROPERTIES - OUTPUT_NAME "panda3ds_libretro" - PREFIX "" - ) + add_library(panda3ds_libretro SHARED src/libretro_core.cpp) + target_link_libraries(panda3ds_libretro PUBLIC AlberCore) + set_target_properties(panda3ds_libretro PROPERTIES PREFIX "") endif() if(ENABLE_LTO OR ENABLE_USER_BUILD) - set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) + if (NOT BUILD_LIBRETRO_CORE) + set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) + else() + set_target_properties(panda3ds_libretro PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) + endif() endif() if(ENABLE_TESTS) diff --git a/docs/3ds/accelerometer_readings/readings_flat_1.png b/docs/3ds/accelerometer_readings/readings_flat_1.png new file mode 100644 index 000000000..b7a425fc4 Binary files /dev/null and b/docs/3ds/accelerometer_readings/readings_flat_1.png differ diff --git a/docs/3ds/accelerometer_readings/readings_flat_2.png b/docs/3ds/accelerometer_readings/readings_flat_2.png new file mode 100644 index 000000000..b23c1102a Binary files /dev/null and b/docs/3ds/accelerometer_readings/readings_flat_2.png differ diff --git a/docs/3ds/accelerometer_readings/readings_shaking_1.png b/docs/3ds/accelerometer_readings/readings_shaking_1.png new file mode 100644 index 000000000..912791492 Binary files /dev/null and b/docs/3ds/accelerometer_readings/readings_shaking_1.png differ diff --git a/docs/3ds/accelerometer_readings/readings_shaking_2.png b/docs/3ds/accelerometer_readings/readings_shaking_2.png new file mode 100644 index 000000000..551e7d2ee Binary files /dev/null and b/docs/3ds/accelerometer_readings/readings_shaking_2.png differ diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index ecdbc02e0..3ff16a1d0 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -122,6 +122,7 @@ class MainWindow : public QMainWindow { void showAboutMenu(); void initControllers(); void pollControllers(); + void setupControllerSensors(SDL_GameController* controller); void sendMessage(const EmulatorMessage& message); void dispatchMessage(const EmulatorMessage& message); diff --git a/include/panda_sdl/frontend_sdl.hpp b/include/panda_sdl/frontend_sdl.hpp index 070389625..cbd0b88e5 100644 --- a/include/panda_sdl/frontend_sdl.hpp +++ b/include/panda_sdl/frontend_sdl.hpp @@ -37,4 +37,6 @@ class FrontendSDL { // And so the user can still use the keyboard to control the analog bool keyboardAnalogX = false; bool keyboardAnalogY = false; + + void setupControllerSensors(SDL_GameController* controller); }; \ No newline at end of file diff --git a/include/sdl_sensors.hpp b/include/sdl_sensors.hpp new file mode 100644 index 000000000..6de040ec5 --- /dev/null +++ b/include/sdl_sensors.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +#include "helpers.hpp" +#include "services/hid.hpp" + +namespace Sensors::SDL { + // Convert the rotation data we get from SDL sensor events to rotation data we can feed right to HID + // Returns [pitch, roll, yaw] + static glm::vec3 convertRotation(glm::vec3 rotation) { + // Convert the rotation from rad/s to deg/s and scale by the gyroscope coefficient in HID + constexpr float scale = 180.f / std::numbers::pi * HIDService::gyroscopeCoeff; + // The axes are also inverted, so invert scale before the multiplication. + return rotation * -scale; + } + + static glm::vec3 convertAcceleration(float* data) { + // Set our cap to ~9 m/s^2. The 3DS sensors cap at -930 and +930, so values above this value will get clamped to 930 + // At rest (3DS laid flat on table), hardware reads around ~0 for x and z axis, and around ~480 for y axis due to gravity. + // This code tries to mimic this approximately, with offsets based on measurements from my DualShock 4. + static constexpr float accelMax = 9.f; + + s16 x = std::clamp(s16(data[0] / accelMax * 930.f), -930, +930); + s16 y = std::clamp(s16(data[1] / (SDL_STANDARD_GRAVITY * accelMax) * 930.f - 350.f), -930, +930); + s16 z = std::clamp(s16((data[2] - 2.1f) / accelMax * 930.f), -930, +930); + + return glm::vec3(x, y, z); + } +} // namespace Sensors::SDL diff --git a/include/services/hid.hpp b/include/services/hid.hpp index 86a55479c..a0eefb1c5 100644 --- a/include/services/hid.hpp +++ b/include/services/hid.hpp @@ -56,6 +56,7 @@ class HIDService { s16 circlePadX, circlePadY; // Circlepad state s16 touchScreenX, touchScreenY; // Touchscreen state s16 roll, pitch, yaw; // Gyroscope state + s16 accelX, accelY, accelZ; // Accelerometer state bool accelerometerEnabled; bool eventsInitialized; @@ -87,7 +88,14 @@ class HIDService { *(T*)&sharedMem[offset] = value; } + template + T* getSharedMemPointer(size_t offset) { + return (T*)&sharedMem[offset]; + } + public: + static constexpr float gyroscopeCoeff = 14.375f; // Same as retail 3DS + HIDService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); @@ -128,6 +136,12 @@ class HIDService { void setPitch(s16 value) { pitch = value; } void setYaw(s16 value) { yaw = value; } + void setAccel(s16 x, s16 y, s16 z) { + accelX = x; + accelY = y; + accelZ = z; + } + void updateInputs(u64 currentTimestamp); void setSharedMem(u8* ptr) { diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index d1297ad83..83271a43e 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -627,5 +627,6 @@ namespace Audio { rateMultiplier = 1.f; buffers = {}; + currentSamples.clear(); } } // namespace Audio diff --git a/src/core/services/hid.cpp b/src/core/services/hid.cpp index ef6cbb414..a7b9b13b1 100644 --- a/src/core/services/hid.cpp +++ b/src/core/services/hid.cpp @@ -35,6 +35,7 @@ void HIDService::reset() { circlePadX = circlePadY = 0; touchScreenX = touchScreenY = 0; roll = pitch = yaw = 0; + accelX = accelY = accelZ = 0; } void HIDService::handleSyncRequest(u32 messagePointer) { @@ -103,7 +104,6 @@ void HIDService::getGyroscopeLowCalibrateParam(u32 messagePointer) { void HIDService::getGyroscopeCoefficient(u32 messagePointer) { log("HID::GetGyroscopeLowRawToDpsCoefficient\n"); - constexpr float gyroscopeCoeff = 14.375f; // Same as retail 3DS mem.write32(messagePointer, IPC::responseHeader(0x15, 2, 0)); mem.write32(messagePointer + 4, Result::Success); mem.write32(messagePointer + 8, Helpers::bit_cast(gyroscopeCoeff)); @@ -190,6 +190,20 @@ void HIDService::updateInputs(u64 currentTick) { writeSharedMem(0x108, currentTick); // Write new tick count } writeSharedMem(0x118, nextAccelerometerIndex); // Index last updated by the HID module + const size_t accelEntryOffset = 0x128 + (nextAccelerometerIndex * 6); // Offset in the array of 8 accelerometer entries + + // Raw data of current accelerometer entry + // TODO: How is the "raw" data actually calculated? + s16* accelerometerDataRaw = getSharedMemPointer(0x120); + accelerometerDataRaw[0] = accelX; + accelerometerDataRaw[1] = accelY; + accelerometerDataRaw[2] = accelZ; + + // Accelerometer entry in entry table + s16* accelerometerData = getSharedMemPointer(accelEntryOffset); + accelerometerData[0] = accelX; + accelerometerData[1] = accelY; + accelerometerData[2] = accelZ; nextAccelerometerIndex = (nextAccelerometerIndex + 1) % 8; // Move to next entry // Next, update gyro state @@ -198,9 +212,10 @@ void HIDService::updateInputs(u64 currentTick) { writeSharedMem(0x158, currentTick); // Write new tick count } const size_t gyroEntryOffset = 0x178 + (nextGyroIndex * 6); // Offset in the array of 8 touchscreen entries - writeSharedMem(gyroEntryOffset, pitch); - writeSharedMem(gyroEntryOffset + 2, yaw); - writeSharedMem(gyroEntryOffset + 4, roll); + s16* gyroData = getSharedMemPointer(gyroEntryOffset); + gyroData[0] = pitch; + gyroData[1] = yaw; + gyroData[2] = roll; // Since gyroscope euler angles are relative, we zero them out here and the frontend will update them again when we receive a new rotation roll = pitch = yaw = 0; diff --git a/src/emulator.cpp b/src/emulator.cpp index fdf56a004..45b63a123 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -1,6 +1,6 @@ #include "emulator.hpp" -#ifndef __ANDROID__ +#if !defined(__ANDROID__) && !defined(__LIBRETRO__) #include #endif diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 6974a9e38..b4c29540b 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -372,7 +372,7 @@ uint retro_api_version() { return RETRO_API_VERSION; } usize retro_get_memory_size(uint id) { if (id == RETRO_MEMORY_SYSTEM_RAM) { - return 0; + return Memory::FCRAM_SIZE; } return 0; @@ -380,7 +380,7 @@ usize retro_get_memory_size(uint id) { void* retro_get_memory_data(uint id) { if (id == RETRO_MEMORY_SYSTEM_RAM) { - return 0; + return emulator->getMemory().getFCRAM(); } return nullptr; diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 65769116e..6bdffb7e7 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -9,6 +9,7 @@ #include "cheats.hpp" #include "input_mappings.hpp" +#include "sdl_sensors.hpp" #include "services/dsp.hpp" MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) { @@ -521,6 +522,8 @@ void MainWindow::initControllers() { SDL_Joystick* stick = SDL_GameControllerGetJoystick(gameController); gameControllerID = SDL_JoystickInstanceID(stick); } + + setupControllerSensors(gameController); } } @@ -558,6 +561,8 @@ void MainWindow::pollControllers() { if (gameController == nullptr) { gameController = SDL_GameControllerOpen(event.cdevice.which); gameControllerID = event.cdevice.which; + + setupControllerSensors(gameController); } break; @@ -598,6 +603,37 @@ void MainWindow::pollControllers() { } break; } + + case SDL_CONTROLLERSENSORUPDATE: { + if (event.csensor.sensor == SDL_SENSOR_GYRO) { + auto rotation = Sensors::SDL::convertRotation({ + event.csensor.data[0], + event.csensor.data[1], + event.csensor.data[2], + }); + + hid.setPitch(s16(rotation.x)); + hid.setRoll(s16(rotation.y)); + hid.setYaw(s16(rotation.z)); + } else if (event.csensor.sensor == SDL_SENSOR_ACCEL) { + auto accel = Sensors::SDL::convertAcceleration(event.csensor.data); + hid.setAccel(accel.x, accel.y, accel.z); + } + break; + } } } } + +void MainWindow::setupControllerSensors(SDL_GameController* controller) { + bool haveGyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; + bool haveAccelerometer = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE; + + if (haveGyro) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); + } + + if (haveAccelerometer) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + } +} \ No newline at end of file diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 77b1f55fd..901668996 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -2,6 +2,8 @@ #include +#include "sdl_sensors.hpp" + FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMappings()) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { Helpers::panic("Failed to initialize SDL2"); @@ -20,6 +22,8 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp SDL_Joystick* stick = SDL_GameControllerGetJoystick(gameController); gameControllerID = SDL_JoystickInstanceID(stick); } + + setupControllerSensors(gameController); } const EmulatorConfig& config = emu.getConfig(); @@ -200,6 +204,8 @@ void FrontendSDL::run() { if (gameController == nullptr) { gameController = SDL_GameControllerOpen(event.cdevice.which); gameControllerID = event.cdevice.which; + + setupControllerSensors(gameController); } break; @@ -280,6 +286,24 @@ void FrontendSDL::run() { } break; } + + case SDL_CONTROLLERSENSORUPDATE: { + if (event.csensor.sensor == SDL_SENSOR_GYRO) { + auto rotation = Sensors::SDL::convertRotation({ + event.csensor.data[0], + event.csensor.data[1], + event.csensor.data[2], + }); + + hid.setPitch(s16(rotation.x)); + hid.setRoll(s16(rotation.y)); + hid.setYaw(s16(rotation.z)); + } else if (event.csensor.sensor == SDL_SENSOR_ACCEL) { + auto accel = Sensors::SDL::convertAcceleration(event.csensor.data); + hid.setAccel(accel.x, accel.y, accel.z); + } + break; + } case SDL_DROPFILE: { char* droppedDir = event.drop.file; @@ -342,3 +366,16 @@ void FrontendSDL::run() { SDL_GL_SwapWindow(window); } } + +void FrontendSDL::setupControllerSensors(SDL_GameController* controller) { + bool haveGyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; + bool haveAccelerometer = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE; + + if (haveGyro) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); + } + + if (haveAccelerometer) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + } +} \ No newline at end of file diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp index 4a08650a3..607815fa0 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp @@ -432,6 +432,25 @@ namespace OpenGL { return m_handle != 0; } + bool createFromBinary(const uint8_t* binary, size_t size, GLenum format) { + m_handle = glCreateProgram(); + glProgramBinary(m_handle, format, binary, size); + + GLint success; + glGetProgramiv(m_handle, GL_LINK_STATUS, &success); + + if (!success) { + char buf[4096]; + glGetProgramInfoLog(m_handle, 4096, nullptr, buf); + fprintf(stderr, "Failed to link program\nError: %s\n", buf); + glDeleteProgram(m_handle); + + m_handle = 0; + } + + return m_handle != 0; + } + GLuint handle() const { return m_handle; } bool exists() const { return m_handle != 0; } void use() const { glUseProgram(m_handle); }