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"},