diff --git a/CMakeLists.txt b/CMakeLists.txt index 75ad6ea20..33f2b01a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,7 +129,8 @@ CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support CMAKE_DEPENDENT_OPTION(ENABLE_SOFTWARE_RENDERER "Enables the software renderer" ON "NOT ANDROID" OFF) CMAKE_DEPENDENT_OPTION(ENABLE_OPENGL "Enables the OpenGL renderer" ${DEFAULT_ENABLE_OPENGL} "NOT APPLE" OFF) -option(ENABLE_VULKAN "Enables the Vulkan renderer" ON) +# NetBSD doesn't support Vulkan yet, remove this check when it does. +CMAKE_DEPENDENT_OPTION(ENABLE_VULKAN "Enables the Vulkan renderer" ON "NOT (BSD MATCHES \"NetBSD\")" OFF) option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF) diff --git a/CMakeModules/DisablePaxMprotect.cmake b/CMakeModules/DisablePaxMprotect.cmake new file mode 100644 index 000000000..e5f83edd0 --- /dev/null +++ b/CMakeModules/DisablePaxMprotect.cmake @@ -0,0 +1,11 @@ +function(disable_pax_mprotect target) + if (BSD STREQUAL "NetBSD") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND paxctl +m "$" + COMMENT "Disabling PaX MPROTECT restrictions for '${target}'" + VERBATIM + ) + else() + message(FATAL_ERROR "disable_pax_mprotect only applies on NetBSD.") + endif() +endfunction() diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index a7f238979..27aea20b0 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -60,6 +60,7 @@ if (ENABLE_TESTS) add_subdirectory(catch2) endif() target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain) + include(Catch) endif() # Crypto++ @@ -504,6 +505,15 @@ if (ENABLE_VULKAN) else() target_include_directories(vulkan-headers INTERFACE ./vulkan-headers/include) target_disable_warnings(vulkan-headers) + if (BSD STREQUAL "NetBSD") + # There may be a better way to do this with + # find_package(X11), but I couldn't get + # CMake to do it, so we're depending on + # the x11-links package and assuming the + # prefix location. -OS + target_include_directories(vulkan-headers INTERFACE + /usr/pkg/share/x11-links/include) + endif() endif() # adrenotools diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 9d2015baa..f8cb55874 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -504,8 +504,9 @@ object NativeLibrary { const val ErrorSystemFiles = 8 const val ErrorSavestate = 9 const val ErrorArticDisconnected = 10 - const val ShutdownRequested = 11 - const val ErrorUnknown = 12 + const val ErrorN3DSApplication = 11 + const val ShutdownRequested = 12 + const val ErrorUnknown = 13 fun newInstance(resultCode: Int): EmulationErrorDialogFragment { val args = Bundle() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt index 2c8cbf2a1..eb1a880a5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -17,7 +17,7 @@ enum class IntSetting( CAMERA_INNER_FLIP(SettingKeys.camera_inner_flip(), Settings.SECTION_CAMERA, 0), CAMERA_OUTER_LEFT_FLIP(SettingKeys.camera_outer_left_flip(), Settings.SECTION_CAMERA, 0), CAMERA_OUTER_RIGHT_FLIP(SettingKeys.camera_outer_right_flip(), Settings.SECTION_CAMERA, 0), - GRAPHICS_API(SettingKeys.graphics_api(), Settings.SECTION_RENDERER, 1), + GRAPHICS_API(SettingKeys.graphics_api(), Settings.SECTION_RENDERER, 2), RESOLUTION_FACTOR(SettingKeys.resolution_factor(), Settings.SECTION_RENDERER, 1), STEREOSCOPIC_3D_MODE(SettingKeys.render_3d(), Settings.SECTION_RENDERER, 2), STEREOSCOPIC_3D_DEPTH(SettingKeys.factor_3d(), Settings.SECTION_RENDERER, 0), diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 5e2eab1d1..ce93d0e6f 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -90,7 +90,7 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"( [Renderer] # Whether to render using OpenGL -# 1: OpenGL ES (default), 2: Vulkan +# 1: OpenGL ES, 2: Vulkan (default) )") DECLARE_KEY(graphics_api) BOOST_HANA_STRING(R"( # Whether to compile shaders on multiple worker threads (Vulkan only) diff --git a/src/android/app/src/main/res/layout/fragment_system_files.xml b/src/android/app/src/main/res/layout/fragment_system_files.xml index bae1cda1c..650766748 100644 --- a/src/android/app/src/main/res/layout/fragment_system_files.xml +++ b/src/android/app/src/main/res/layout/fragment_system_files.xml @@ -5,13 +5,13 @@ android:id="@+id/coordinator_about" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="?attr/colorSurface"> + android:layout_height="wrap_content"> ()); Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); + system.RegisterInfoLEDColorChanged([this]() { emit InfoLEDColorChanged(); }); + LoadTranslation(); Pica::g_debug_context = Pica::DebugContext::Construct(); @@ -606,6 +608,20 @@ void GMainWindow::InitializeWidgets() { statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); + QFrame* sep = new QFrame(this); + sep->setFrameShape(QFrame::VLine); + sep->setFrameShadow(QFrame::Sunken); + sep->setFixedHeight(16); + statusBar()->addPermanentWidget(sep); + + notification_led = new LedWidget(); + notification_led->setToolTip(tr("Emulated notification LED")); + statusBar()->addPermanentWidget(notification_led); + connect(this, &GMainWindow::InfoLEDColorChanged, this, [this] { + auto led_color = system.GetInfoLEDColor(); + notification_led->setColor(QColor(led_color.r(), led_color.g(), led_color.b())); + }); + statusBar()->setVisible(true); // Removes an ugly inner border from the status bar widgets under Linux @@ -1600,6 +1616,7 @@ void GMainWindow::ShutdownGame() { emu_speed_label->setVisible(false); game_fps_label->setVisible(false); emu_frametime_label->setVisible(false); + notification_led->setColor(QColor(0, 0, 0)); UpdateSaveStates(); diff --git a/src/citra_qt/citra_qt.h b/src/citra_qt/citra_qt.h index cdf9eaff6..f0092cc19 100644 --- a/src/citra_qt/citra_qt.h +++ b/src/citra_qt/citra_qt.h @@ -21,6 +21,7 @@ #include #include "citra_qt/compatibility_list.h" #include "citra_qt/hotkeys.h" +#include "citra_qt/notification_led.h" #include "citra_qt/user_data_migration.h" #include "core/core.h" #include "core/savestate.h" @@ -147,6 +148,7 @@ signals: void CIAInstallReport(Service::AM::InstallStatus status, QString filepath); void CompressFinished(bool is_compress, bool success); void CIAInstallFinished(); + void InfoLEDColorChanged(); // Signal that tells widgets to update icons to use the current theme void UpdateThemedIcons(); @@ -364,6 +366,8 @@ private: MultiplayerState* multiplayer_state = nullptr; + LedWidget* notification_led = nullptr; + // Created before `config` to ensure that emu data directory // isn't created before the check is performed UserDataMigrator user_data_migrator; diff --git a/src/citra_qt/notification_led.cpp b/src/citra_qt/notification_led.cpp new file mode 100644 index 000000000..e7d9e0d33 --- /dev/null +++ b/src/citra_qt/notification_led.cpp @@ -0,0 +1,87 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "citra_qt/notification_led.h" + +LedWidget::LedWidget(QWidget* parent) : QWidget(parent), color(0, 0, 0) { + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); +} + +QSize LedWidget::sizeHint() const { + return QSize(16, 16); +} + +QSize LedWidget::minimumSizeHint() const { + return QSize(16, 16); +} + +void LedWidget::setColor(const QColor& _color) { + if (color == _color) + return; + + color = _color; + update(); +} + +QColor LedWidget::lerpColor(const QColor& a, const QColor& b, float t) { + t = std::clamp(t, 0.0f, 1.0f); + + return QColor(int(a.red() + (b.red() - a.red()) * t), + int(a.green() + (b.green() - a.green()) * t), + int(a.blue() + (b.blue() - a.blue()) * t)); +} + +QColor LedWidget::blendLedColor(int r, int g, int b) const { + // Default "off" color + const QColor off_color(64, 64, 64); + + // If completely off, just show gray and skip further calculations + if (r == 0 && g == 0 && b == 0) + return off_color; + + // Normalize lit color so hue stays pure + int max_c = std::max({r, g, b}); + QColor lit_color((r * 255) / max_c, (g * 255) / max_c, (b * 255) / max_c); + + // Convert PWM duty to perceived brightness. + // This gives better results as LED RGB values + // are not linear. + constexpr float gamma = 2.4f; + float pwm = max_c / 255.0; + float t = std::powf(pwm, 1.f / gamma); + + return lerpColor(off_color, lit_color, t * 0.8f); +} + +void LedWidget::paintEvent(QPaintEvent*) { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + + QRectF rect = this->rect().adjusted(0, 2, 0, -2); + + qreal size = std::min(rect.width(), rect.height()); + QRectF circle((rect.center().x() - size / 2.f) - 2, rect.center().y() - size / 2.f, size, size); + + QPointF center = circle.center(); + qreal radius = circle.width() / 2.f; + + QColor base = blendLedColor(color.red(), color.green(), color.blue()); + + QRadialGradient g(center, radius); + + QColor inner = base.lighter(135); + QColor outer = base.darker(125); + + g.setColorAt(0.f, inner); + g.setColorAt(0.7f, base); + g.setColorAt(1.f, outer); + + p.setPen(Qt::NoPen); + p.setBrush(g); + p.drawEllipse(circle); +} diff --git a/src/citra_qt/notification_led.h b/src/citra_qt/notification_led.h new file mode 100644 index 000000000..3a139a202 --- /dev/null +++ b/src/citra_qt/notification_led.h @@ -0,0 +1,30 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +class LedWidget : public QWidget { + Q_OBJECT + +public: + explicit LedWidget(QWidget* parent = nullptr); + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + + void setColor(const QColor& color); + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + QColor blendLedColor(int r, int g, int b) const; + static QColor lerpColor(const QColor& a, const QColor& b, float t); + +private: + QColor color; +}; diff --git a/src/citra_room_standalone/CMakeLists.txt b/src/citra_room_standalone/CMakeLists.txt index e48051469..3660b11aa 100644 --- a/src/citra_room_standalone/CMakeLists.txt +++ b/src/citra_room_standalone/CMakeLists.txt @@ -4,6 +4,11 @@ add_executable(citra_room_standalone set_target_properties(citra_room_standalone PROPERTIES OUTPUT_NAME "azahar-room") +if (BSD STREQUAL "NetBSD") + include(DisablePaxMprotect) + disable_pax_mprotect(citra_room_standalone) +endif() + target_link_libraries(citra_room_standalone PRIVATE citra_room) if(UNIX AND NOT APPLE) diff --git a/src/common/settings.h b/src/common/settings.h index 90922bcff..5eca53421 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -498,8 +498,11 @@ struct Values { Setting apply_region_free_patch{true, Keys::apply_region_free_patch}; // Renderer + // clang-format off SwitchableSetting graphics_api{ -#if defined(ENABLE_OPENGL) +#if defined(ANDROID) && defined(ENABLE_VULKAN) // Prefer Vulkan on Android, OpenGL on everything else + GraphicsAPI::Vulkan, +#elif defined(ENABLE_OPENGL) GraphicsAPI::OpenGL, #elif defined(ENABLE_VULKAN) GraphicsAPI::Vulkan, @@ -510,6 +513,7 @@ struct Values { #error "At least one renderer must be enabled." #endif GraphicsAPI::Software, GraphicsAPI::Vulkan, Keys::graphics_api}; + // clang-format on SwitchableSetting physical_device{0, Keys::physical_device}; Setting use_gles{false, Keys::use_gles}; Setting renderer_debug{false, Keys::renderer_debug}; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index fd8212dcf..79ed4695a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -350,6 +350,8 @@ add_library(citra_core STATIC hle/service/ldr_ro/ldr_ro.h hle/service/mcu/mcu_hwc.cpp hle/service/mcu/mcu_hwc.h + hle/service/mcu/mcu_rtc.cpp + hle/service/mcu/mcu_rtc.h hle/service/mcu/mcu.cpp hle/service/mcu/mcu.h hle/service/mic/mic_u.cpp diff --git a/src/core/core.cpp b/src/core/core.cpp index 42cba3160..0132e24bd 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -590,6 +590,8 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, plg_ldr->SetAllowGameChangeState(Settings::values.allow_plugin_loader.GetValue()); } + SetInfoLEDColor({}); + LOG_DEBUG(Core, "Initialized OK"); is_powered_on = true; @@ -720,6 +722,8 @@ void System::Shutdown(bool is_deserializing) { memory.reset(); + SetInfoLEDColor({}); + LOG_DEBUG(Core, "Shutdown OK"); } diff --git a/src/core/core.h b/src/core/core.h index ca61e8262..05620da32 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -12,6 +12,7 @@ #include #include #include "common/common_types.h" +#include "common/vector_math.h" #include "core/arm/arm_interface.h" #include "core/cheats/cheats.h" #include "core/hle/service/apt/applet_manager.h" @@ -381,6 +382,27 @@ public: bool IsInitialSetup(); + // This returns the 3DS notification LED RGB value. + // Keep in mind this is used as a PWM duty cycle on real HW, + // so the percieved LED brightness is not linear. + const Common::Vec3& GetInfoLEDColor() const { + return info_led_color; + } + + void SetInfoLEDColor(const Common::Vec3& color) { + if (color == info_led_color) + return; + + info_led_color = color; + if (info_led_color_changed) { + info_led_color_changed(); + } + } + + void RegisterInfoLEDColorChanged(const std::function& func) { + info_led_color_changed = func; + } + private: /** * Initialize the emulated system. @@ -487,6 +509,9 @@ private: std::vector lle_modules; + Common::Vec3 info_led_color; + std::function info_led_color_changed; + friend class boost::serialization::access; template void serialize(Archive& ar, const unsigned int file_version); diff --git a/src/core/hle/service/mcu/mcu.cpp b/src/core/hle/service/mcu/mcu.cpp index dff4ee3e9..83b0742b6 100644 --- a/src/core/hle/service/mcu/mcu.cpp +++ b/src/core/hle/service/mcu/mcu.cpp @@ -1,16 +1,18 @@ -// Copyright 2024 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "core/core.h" #include "core/hle/service/mcu/mcu.h" #include "core/hle/service/mcu/mcu_hwc.h" +#include "core/hle/service/mcu/mcu_rtc.h" namespace Service::MCU { void InstallInterfaces(Core::System& system) { auto& service_manager = system.ServiceManager(); - std::make_shared()->InstallAsService(service_manager); + std::make_shared(system)->InstallAsService(service_manager); + std::make_shared(system)->InstallAsService(service_manager); } } // namespace Service::MCU diff --git a/src/core/hle/service/mcu/mcu_hwc.cpp b/src/core/hle/service/mcu/mcu_hwc.cpp index c2a5d7514..dbdf6b2af 100644 --- a/src/core/hle/service/mcu/mcu_hwc.cpp +++ b/src/core/hle/service/mcu/mcu_hwc.cpp @@ -1,15 +1,18 @@ -// Copyright 2024 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "common/archives.h" +#include "core/hle/ipc_helpers.h" #include "core/hle/service/mcu/mcu_hwc.h" +#include "core/hle/service/mcu/mcu_rtc.h" +SERVICE_CONSTRUCT_IMPL(Service::MCU::HWC) SERIALIZE_EXPORT_IMPL(Service::MCU::HWC) namespace Service::MCU { -HWC::HWC() : ServiceFramework("mcu::HWC", 1) { +HWC::HWC(Core::System& _system) : ServiceFramework("mcu::HWC", 1), system(_system) { static const FunctionInfo functions[] = { // clang-format off {0x0001, nullptr, "ReadRegister"}, @@ -21,7 +24,7 @@ HWC::HWC() : ServiceFramework("mcu::HWC", 1) { {0x0007, nullptr, "SetWifiLEDState"}, {0x0008, nullptr, "SetCameraLEDPattern"}, {0x0009, nullptr, "Set3DLEDState"}, - {0x000A, nullptr, "SetInfoLEDPattern"}, + {0x000A, &HWC::SetInfoLEDPattern, "SetInfoLEDPattern"}, {0x000B, nullptr, "GetSoundVolume"}, {0x000C, nullptr, "SetTopScreenFlicker"}, {0x000D, nullptr, "SetBottomScreenFlicker"}, @@ -33,4 +36,19 @@ HWC::HWC() : ServiceFramework("mcu::HWC", 1) { RegisterHandlers(functions); } +void HWC::SetInfoLEDPattern(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto pat = rp.PopRaw(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto mcu_rtc = MCU::RTC::GetService(system); + if (mcu_rtc) { + mcu_rtc->UpdateInfoLEDPattern(pat); + rb.Push(ResultSuccess); + } else { + rb.Push(ResultUnknown); + } +} + } // namespace Service::MCU diff --git a/src/core/hle/service/mcu/mcu_hwc.h b/src/core/hle/service/mcu/mcu_hwc.h index fdcd7bf2f..8e117e9f9 100644 --- a/src/core/hle/service/mcu/mcu_hwc.h +++ b/src/core/hle/service/mcu/mcu_hwc.h @@ -1,4 +1,4 @@ -// Copyright 2024 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -10,12 +10,17 @@ namespace Service::MCU { class HWC final : public ServiceFramework { public: - explicit HWC(); + explicit HWC(Core::System& _system); private: + Core::System& system; + + void SetInfoLEDPattern(Kernel::HLERequestContext& ctx); + SERVICE_SERIALIZATION_SIMPLE }; } // namespace Service::MCU +SERVICE_CONSTRUCT(Service::MCU::HWC) BOOST_CLASS_EXPORT_KEY(Service::MCU::HWC) diff --git a/src/core/hle/service/mcu/mcu_rtc.cpp b/src/core/hle/service/mcu/mcu_rtc.cpp new file mode 100644 index 000000000..294f4c41f --- /dev/null +++ b/src/core/hle/service/mcu/mcu_rtc.cpp @@ -0,0 +1,293 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "common/archives.h" +#include "common/vector_math.h" +#include "core/core.h" +#include "core/core_timing.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/mcu/mcu.h" +#include "core/hle/service/mcu/mcu_rtc.h" + +SERVICE_CONSTRUCT_IMPL(Service::MCU::RTC) +SERIALIZE_EXPORT_IMPL(Service::MCU::RTC) + +namespace Service::MCU { + +class InfoLedHandler { +public: + InfoLedHandler() = default; + ~InfoLedHandler() = default; + + static constexpr s64 CALLBACK_PERIOD_NS = 1'000'000'000ll / 60; // 60Hz (~16ms) + static constexpr s64 MCU_TICK_PERIOD_NS = 1'000'000'000ll / 512; // 512Hz (~2ms) + + void SetPattern(const InfoLedPattern& p) { + current_pattern = p; + pattern_changed = true; + } + + void SetHeader(const InfoLedPattern::Header& header) { + current_pattern.header = header; + pattern_changed = true; + } + + // The MCU led code is updated with a frequency of 512Hz on real hardware. However + // it is not a very relevant feature for emulation, so to prevent slicing the core + // timing too much let's update it every frame instead (60Hz) and adjust for it. + void Tick(s64 cycles_late) { + + const s64 late_ns = cyclesToNs(cycles_late); + + // Accumulate elapsed time. + arm_time_ns += CALLBACK_PERIOD_NS + late_ns; + if (arm_time_ns < 0) + arm_time_ns = 0; + + // Sync the MCU state up to the current ARM time + while (arm_time_ns >= MCU_TICK_PERIOD_NS) { + arm_time_ns -= MCU_TICK_PERIOD_NS; + TickMCULed(); + } + } + + Common::Vec3 Color() const { + return result_color; + } + + // To save CPU time, do not tick if all smooth state has finished + // and the pattern is all zero. + bool NeedsTicking() { + auto patAllZero = [this]() -> bool { + u32* data = reinterpret_cast(¤t_pattern); + for (size_t i = 0; i < sizeof(InfoLedPattern) / sizeof(u32); i++) { + if (data[i]) + return false; + } + return true; + }; + + return !patAllZero() || !state_r.Finished() || !state_g.Finished() || !state_b.Finished(); + } + + bool Status() const { + return status_finished; + } + +private: + struct LedSmoothState { + s16 target = 0; + s16 increment = 0; + s16 current = 0; + + bool Finished() { + return current == target; + } + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int) { + ar & target; + ar & increment; + ar & current; + } + }; + + // Decompilation of MCU function at address 0x2f44 + void setSmoothState(LedSmoothState& state, u8 color) { + // Looks like the color is multiplied for better precision + state.target = static_cast(color) * 128; + + // Real HW makes sure ticks_to_progress is not 0 when the led pattern + // is set through I2C. We check for it here instead as it's equivalent. + const u8 ticks = std::max(current_pattern.header.ticks_to_progress, 1); + state.increment = (state.target - state.current) / ticks; + } + + // Decompilation of MCU function at address 0x2dc0 + static u8 updateSmoothState(LedSmoothState& status) { + if (!status.Finished()) { + if (std::abs(status.target - status.current) > std::abs(status.increment)) { + status.current += status.increment; + } else { + status.current = status.target; + } + } + + return static_cast(status.current / 128); + } + + // Decompilation of MCU function at address 0x2f6b + // This function is called every 1/512 seconds + void TickMCULed() { + + // Here, a few things happen. + // If a global variable is set to 2 (0xff904), the led state is cleared. + // If a global variable bit 0 is set (0xffe98), this function does not run at all. + // If a global variable bit 7 is set (0xffe97), this function takes another path which + // runs function 0x2f1d instead of setSmoothState() to set the LED smooth status. + // This function seems to setup smooth to fade to off state. + // TODO(PabloMK7): Figure out what those mean. Maybe power on/off related + + if (pattern_changed) { + pattern_changed = false; + status_finished = false; + ticks_to_next_index = 0; + index = 0; + } else { + if (ticks_to_next_index == 0) { + ticks_to_next_index = current_pattern.header.ticks_per_index; + + if (index < InfoLedPattern::PATTERN_INDEX_COUNT - 1) { + status_finished = false; + index = (index + 1) % InfoLedPattern::PATTERN_INDEX_COUNT; + last_index_repeat_times = 0; + } else { + status_finished = true; + if (current_pattern.header.last_index_repeat_times != 0xFF) { + last_index_repeat_times++; + if (last_index_repeat_times > + current_pattern.header.last_index_repeat_times) { + index = 0; + } + } + } + + // Set smooth for the next index + setSmoothState(state_r, current_pattern.r[index]); + setSmoothState(state_g, current_pattern.g[index]); + setSmoothState(state_b, current_pattern.b[index]); + } + ticks_to_next_index--; + } + + // Update smooth state + result_color.r() = updateSmoothState(state_r); + result_color.g() = updateSmoothState(state_g); + result_color.b() = updateSmoothState(state_b); + } + +private: + InfoLedPattern current_pattern{}; + + bool pattern_changed = false; + bool status_finished = false; + + u8 ticks_to_next_index = 0; + u8 index = 0; + u8 last_index_repeat_times = 0; + + LedSmoothState state_r{}; + LedSmoothState state_g{}; + LedSmoothState state_b{}; + + Common::Vec3 result_color{}; + + s64 arm_time_ns = 0; + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int) { + ar & current_pattern; + ar & pattern_changed; + ar & status_finished; + ar & ticks_to_next_index; + ar & index; + ar & last_index_repeat_times; + ar & state_r; + ar & state_g; + ar & state_b; + ar & result_color; + ar & arm_time_ns; + } +}; + +RTC::RTC(Core::System& _system) : ServiceFramework("mcu::RTC", 1), system(_system) { + static const FunctionInfo functions[] = { + // clang-format off + {0x003B, &RTC::SetInfoLEDPattern, "SetInfoLEDPattern"}, + {0x003C, &RTC::SetInfoLEDPatternHeader, "SetInfoLEDPattern"}, + {0x003D, &RTC::GetInfoLEDStatus, "SetInfoLEDPattern"}, + // clang-format on + }; + RegisterHandlers(functions); + + info_led = std::make_unique(); + info_led_tick_event = + system.Kernel().timing.RegisterEvent("MCUTickInfoLED", [this](u64, s64 cycles_late) { + info_led->Tick(cycles_late); + system.SetInfoLEDColor(info_led->Color()); + if (info_led->NeedsTicking()) { + system.Kernel().timing.ScheduleEvent(nsToCycles(InfoLedHandler::CALLBACK_PERIOD_NS), + info_led_tick_event, 0, 1); + } else { + info_led_ticking = false; + } + }); +} + +RTC::~RTC() {} + +void RTC::UpdateInfoLEDPattern(const InfoLedPattern& pat) { + info_led->SetPattern(pat); + if (!info_led_ticking) { + system.Kernel().timing.ScheduleEvent(0, info_led_tick_event, 0, 1); + info_led_ticking = true; + } +} + +void RTC::UpdateInfoLEDHeader(const InfoLedPattern::Header& header) { + info_led->SetHeader(header); + if (!info_led_ticking) { + system.Kernel().timing.ScheduleEvent(0, info_led_tick_event, 0, 1); + info_led_ticking = true; + } +} + +bool RTC::GetInfoLEDStatusFinished() { + return info_led->Status(); +} + +std::shared_ptr RTC::GetService(Core::System& system) { + return system.ServiceManager().GetService("mcu::RTC"); +} + +void RTC::SetInfoLEDPattern(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto pat = rp.PopRaw(); + + UpdateInfoLEDPattern(pat); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void RTC::SetInfoLEDPatternHeader(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto head = rp.PopRaw(); + + UpdateInfoLEDHeader(head); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void RTC::GetInfoLEDStatus(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(static_cast(GetInfoLEDStatusFinished())); +} + +template +void RTC::serialize(Archive& ar, const unsigned int) { + DEBUG_SERIALIZATION_POINT; + ar& boost::serialization::base_object(*this); + ar & info_led; + ar & info_led_ticking; +} + +} // namespace Service::MCU diff --git a/src/core/hle/service/mcu/mcu_rtc.h b/src/core/hle/service/mcu/mcu_rtc.h new file mode 100644 index 000000000..651b650df --- /dev/null +++ b/src/core/hle/service/mcu/mcu_rtc.h @@ -0,0 +1,83 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/core_timing.h" +#include "core/hle/service/service.h" + +namespace Service::MCU { +class InfoLedHandler; + +struct InfoLedPattern { + static constexpr size_t PATTERN_INDEX_COUNT = 32; + + struct Header { + u8 ticks_per_index{}; // Amount of ticks to stay in the current index (1 tick == 1/512 s) + u8 ticks_to_progress{}; // Amount of ticks to go from the previous value to the current + // index value. Normally, this only makes sense to be set to 0 to + // disable interpolation, or equal to "ticks_to_progress" for linear + // interpolation. Any other value breaks the interpolation math. + u8 last_index_repeat_times{}; // Amount of times to repeat the last index, as if the color + // array had "last_index_repeat_times" more elements equal to + // the last array value. (0xFF means repeat forever) + u8 padding{}; + } header; + + // RGB color elements, corresponding to the LED PWM duty cycle. + // (0x0 -> fully off, 0xFF -> fully on) + std::array r{}; + std::array g{}; + std::array b{}; + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned int) { + ar & header.ticks_per_index; + ar & header.ticks_to_progress; + ar & header.last_index_repeat_times; + ar & header.padding; + + ar & r; + ar & g; + ar & b; + } +}; +static_assert(sizeof(InfoLedPattern) == 0x64); + +class RTC final : public ServiceFramework { +public: + explicit RTC(Core::System& _system); + ~RTC(); + + void UpdateInfoLEDPattern(const InfoLedPattern& pat); + + void UpdateInfoLEDHeader(const InfoLedPattern::Header& header); + + bool GetInfoLEDStatusFinished(); + + static std::shared_ptr GetService(Core::System& system); + +private: + void SetInfoLEDPattern(Kernel::HLERequestContext& ctx); + + void SetInfoLEDPatternHeader(Kernel::HLERequestContext& ctx); + + void GetInfoLEDStatus(Kernel::HLERequestContext& ctx); + + Core::System& system; + + std::unique_ptr info_led; + Core::TimingEventType* info_led_tick_event{}; + bool info_led_ticking{}; + + template + void serialize(Archive& ar, const unsigned int); + friend class boost::serialization::access; +}; + +} // namespace Service::MCU + +SERVICE_CONSTRUCT(Service::MCU::RTC) +BOOST_CLASS_EXPORT_KEY(Service::MCU::RTC) diff --git a/src/core/hle/service/ptm/ptm.cpp b/src/core/hle/service/ptm/ptm.cpp index 1cdb64c88..f0e5769e1 100644 --- a/src/core/hle/service/ptm/ptm.cpp +++ b/src/core/hle/service/ptm/ptm.cpp @@ -12,6 +12,7 @@ #include "core/file_sys/errors.h" #include "core/file_sys/file_backend.h" #include "core/hle/kernel/shared_page.h" +#include "core/hle/service/mcu/mcu_rtc.h" #include "core/hle/service/ptm/ptm.h" #include "core/hle/service/ptm/ptm_gets.h" #include "core/hle/service/ptm/ptm_play.h" @@ -133,6 +134,51 @@ void Module::Interface::CheckNew3DS(Kernel::HLERequestContext& ctx) { Service::PTM::CheckNew3DS(rb); } +void Module::Interface::SetInfoLEDPattern(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto pat = rp.PopRaw(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto mcu_rtc = MCU::RTC::GetService(ptm->system); + if (mcu_rtc) { + mcu_rtc->UpdateInfoLEDPattern(pat); + rb.Push(ResultSuccess); + } else { + rb.Push(ResultUnknown); + } +} + +void Module::Interface::SetInfoLEDPatternHeader(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + auto head = rp.PopRaw(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto mcu_rtc = MCU::RTC::GetService(ptm->system); + if (mcu_rtc) { + mcu_rtc->UpdateInfoLEDHeader(head); + rb.Push(ResultSuccess); + } else { + rb.Push(ResultUnknown); + } +} + +void Module::Interface::GetInfoLEDStatus(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + + auto mcu_rtc = MCU::RTC::GetService(ptm->system); + if (mcu_rtc) { + rb.Push(ResultSuccess); + rb.Push(static_cast(mcu_rtc->GetInfoLEDStatusFinished())); + } else { + rb.Push(ResultUnknown); + rb.Push(u8{}); + } +} + void Module::Interface::GetSystemTime(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); diff --git a/src/core/hle/service/ptm/ptm.h b/src/core/hle/service/ptm/ptm.h index 2e3e5af55..95863350a 100644 --- a/src/core/hle/service/ptm/ptm.h +++ b/src/core/hle/service/ptm/ptm.h @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -139,6 +139,12 @@ public: */ void CheckNew3DS(Kernel::HLERequestContext& ctx); + void SetInfoLEDPattern(Kernel::HLERequestContext& ctx); + + void SetInfoLEDPatternHeader(Kernel::HLERequestContext& ctx); + + void GetInfoLEDStatus(Kernel::HLERequestContext& ctx); + /** * PTM::GetSystemTime service function * Outputs: diff --git a/src/core/hle/service/ptm/ptm_sysm.cpp b/src/core/hle/service/ptm/ptm_sysm.cpp index f827517d3..aced50518 100644 --- a/src/core/hle/service/ptm/ptm_sysm.cpp +++ b/src/core/hle/service/ptm/ptm_sysm.cpp @@ -1,4 +1,4 @@ -// Copyright 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -41,9 +41,9 @@ PTM_S_Common::PTM_S_Common(std::shared_ptr ptm, const char* name) {0x0408, nullptr, "Awake"}, {0x0409, nullptr, "RebootAsync"}, {0x040A, &PTM_S_Common::CheckNew3DS, "CheckNew3DS"}, - {0x0801, nullptr, "SetInfoLEDPattern"}, - {0x0802, nullptr, "SetInfoLEDPatternHeader"}, - {0x0803, nullptr, "GetInfoLEDStatus"}, + {0x0801, &PTM_S_Common::SetInfoLEDPattern, "SetInfoLEDPattern"}, + {0x0802, &PTM_S_Common::SetInfoLEDPatternHeader, "SetInfoLEDPatternHeader"}, + {0x0803, &PTM_S_Common::GetInfoLEDStatus, "GetInfoLEDStatus"}, {0x0804, nullptr, "SetBatteryEmptyLEDPattern"}, {0x0805, nullptr, "ClearStepHistory"}, {0x0806, nullptr, "SetStepHistory"}, diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index f248ece4b..027609c26 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -23,6 +23,11 @@ add_executable(tests create_target_directory_groups(tests) +if (BSD STREQUAL "NetBSD") + include(DisablePaxMprotect) + disable_pax_mprotect(tests) +endif() + target_link_libraries(tests PRIVATE citra_common citra_core video_core audio_core) target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch2 nihstro-headers Threads::Threads) @@ -31,6 +36,10 @@ if (ENABLE_LIBRETRO) endif() add_test(NAME tests COMMAND tests) +if(NOT ANDROID) + catch_discover_tests(tests) +endif() + if (CITRA_USE_PRECOMPILED_HEADERS) target_precompile_headers(tests PRIVATE precompiled_headers.h) diff --git a/src/tests/video_core/shader.cpp b/src/tests/video_core/shader.cpp index db868a260..4ea976668 100644 --- a/src/tests/video_core/shader.cpp +++ b/src/tests/video_core/shader.cpp @@ -481,6 +481,39 @@ SHADER_TEST_CASE("RSQ", "[video_core][shader]") { REQUIRE(shader.Run({0.0625f}).x == Catch::Approx(4.0f).margin(0.004f)); } +SHADER_TEST_CASE("SETEMIT", "[video_core][shader]") { + Pica::GeometryEmitter geometry_emitter; + + for (u8 winding = 0; winding <= 1; ++winding) { + for (u8 prim_emit = 0; prim_emit <= 1; ++prim_emit) { + for (u8 vertex_id = 0; vertex_id <= 3; ++vertex_id) { + auto shader_setup = CompileShaderSetup({ + {OpCode::Id::NOP}, // setemit + {OpCode::Id::END}, + }); + + // nihstro does not support the SETEMIT instructions, so the instruction-binary must + // be manually + // inserted here: + nihstro::Instruction SETEMIT = {}; + SETEMIT.opcode = nihstro::OpCode(nihstro::OpCode::Id::SETEMIT); + SETEMIT.setemit.winding.Assign(winding); + SETEMIT.setemit.prim_emit.Assign(prim_emit); + SETEMIT.setemit.vertex_id.Assign(vertex_id); + shader_setup->UpdateProgramCode(0, SETEMIT.hex); + + auto shader = TestType(std::move(shader_setup)); + Pica::ShaderUnit shader_unit(&geometry_emitter); + shader.Run(shader_unit, 1.0f); + + REQUIRE(geometry_emitter.emit_state.winding == winding); + REQUIRE(geometry_emitter.emit_state.prim_emit == prim_emit); + REQUIRE(geometry_emitter.emit_state.vertex_id == vertex_id); + } + } + } +} + SHADER_TEST_CASE("Uniform Read", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_c0 = SourceRegister::MakeFloat(0); diff --git a/src/video_core/pica/packed_attribute.h b/src/video_core/pica/packed_attribute.h index 7c8028e38..137c3fb25 100644 --- a/src/video_core/pica/packed_attribute.h +++ b/src/video_core/pica/packed_attribute.h @@ -1,9 +1,10 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include #include #include "common/vector_math.h" diff --git a/src/video_core/pica/shader_unit.cpp b/src/video_core/pica/shader_unit.cpp index 5d81f857a..725dd9ebb 100644 --- a/src/video_core/pica/shader_unit.cpp +++ b/src/video_core/pica/shader_unit.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -29,15 +29,15 @@ void ShaderUnit::WriteOutput(const ShaderRegs& config, AttributeBuffer& buffer) } void GeometryEmitter::Emit(std::span, 16> output_regs) { - ASSERT(vertex_id < 3); + ASSERT(emit_state.vertex_id < 3); u32 output_index{}; for (u32 reg : Common::BitSet(output_mask)) { - buffer[vertex_id][output_index++] = output_regs[reg]; + buffer[emit_state.vertex_id][output_index++] = output_regs[reg]; } - if (prim_emit) { - if (winding) { + if (emit_state.prim_emit) { + if (emit_state.winding) { handlers->winding_setter(); } for (std::size_t i = 0; i < buffer.size(); ++i) { diff --git a/src/video_core/pica/shader_unit.h b/src/video_core/pica/shader_unit.h index 2f9c1843f..80eea8d23 100644 --- a/src/video_core/pica/shader_unit.h +++ b/src/video_core/pica/shader_unit.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -75,11 +75,18 @@ struct GeometryEmitter { void Emit(std::span, 16> output_regs); public: - std::array buffer; - u8 vertex_id; - bool prim_emit; - bool winding; + union EmitState { + struct { + bool winding : 1; + bool prim_emit : 1; + u8 vertex_id : 2; + }; + u8 raw; + } emit_state; + static_assert(sizeof(emit_state) == 1); + u32 output_mask; + std::array buffer; Handlers* handlers; private: @@ -87,9 +94,7 @@ private: template void serialize(Archive& ar, const u32 file_version) { ar & buffer; - ar & vertex_id; - ar & prim_emit; - ar & winding; + ar & emit_state.raw; ar & output_mask; } }; diff --git a/src/video_core/shader/shader_interpreter.cpp b/src/video_core/shader/shader_interpreter.cpp index cb06a62bc..6d373e28f 100644 --- a/src/video_core/shader/shader_interpreter.cpp +++ b/src/video_core/shader/shader_interpreter.cpp @@ -671,9 +671,9 @@ static void RunInterpreter(const ShaderSetup& setup, ShaderUnit& state, case OpCode::Id::SETEMIT: { auto* emitter = state.emitter_ptr; ASSERT_MSG(emitter, "Execute SETEMIT on VS"); - emitter->vertex_id = instr.setemit.vertex_id; - emitter->prim_emit = instr.setemit.prim_emit != 0; - emitter->winding = instr.setemit.winding != 0; + emitter->emit_state.vertex_id = instr.setemit.vertex_id; + emitter->emit_state.prim_emit = instr.setemit.prim_emit != 0; + emitter->emit_state.winding = instr.setemit.winding != 0; break; } diff --git a/src/video_core/shader/shader_jit_a64_compiler.cpp b/src/video_core/shader/shader_jit_a64_compiler.cpp index 636d9cb57..90c8425bc 100644 --- a/src/video_core/shader/shader_jit_a64_compiler.cpp +++ b/src/video_core/shader/shader_jit_a64_compiler.cpp @@ -865,12 +865,13 @@ void JitShader::Compile_SETE(Instruction instr) { l(have_emitter); - MOV(XSCRATCH1.toW(), instr.setemit.vertex_id); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, vertex_id))); - MOV(XSCRATCH1.toW(), instr.setemit.prim_emit); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, prim_emit))); - MOV(XSCRATCH1.toW(), instr.setemit.winding); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, winding))); + const GeometryEmitter::EmitState new_state{ + .winding = instr.setemit.winding != 0, + .prim_emit = instr.setemit.prim_emit != 0, + .vertex_id = static_cast(instr.setemit.vertex_id), + }; + MOV(XSCRATCH1.toW(), new_state.raw); + STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, emit_state))); l(end); } diff --git a/src/video_core/shader/shader_jit_x64_compiler.cpp b/src/video_core/shader/shader_jit_x64_compiler.cpp index 84cdc09bc..1c1ff92b2 100644 --- a/src/video_core/shader/shader_jit_x64_compiler.cpp +++ b/src/video_core/shader/shader_jit_x64_compiler.cpp @@ -905,9 +905,12 @@ void JitShader::Compile_SETE(Instruction instr) { jmp(end); L(have_emitter); - mov(byte[rax + offsetof(GeometryEmitter, vertex_id)], instr.setemit.vertex_id); - mov(byte[rax + offsetof(GeometryEmitter, prim_emit)], instr.setemit.prim_emit); - mov(byte[rax + offsetof(GeometryEmitter, winding)], instr.setemit.winding); + const GeometryEmitter::EmitState new_state{ + .winding = instr.setemit.winding != 0, + .prim_emit = instr.setemit.prim_emit != 0, + .vertex_id = static_cast(instr.setemit.vertex_id), + }; + mov(byte[rax + offsetof(GeometryEmitter, emit_state)], new_state.raw); L(end); }