core: Add notification LED emulation

This commit is contained in:
PabloMK7 2026-04-09 15:24:21 +02:00
parent c650473fdc
commit d29e15f219
11 changed files with 496 additions and 12 deletions

View File

@ -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

View File

@ -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");
}

View File

@ -12,6 +12,7 @@
#include <boost/optional.hpp>
#include <boost/serialization/version.hpp>
#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<u8>& GetInfoLEDColor() const {
return info_led_color;
}
void SetInfoLEDColor(const Common::Vec3<u8>& 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<void()>& func) {
info_led_color_changed = func;
}
private:
/**
* Initialize the emulated system.
@ -487,6 +509,9 @@ private:
std::vector<u64> lle_modules;
Common::Vec3<u8> info_led_color;
std::function<void()> info_led_color_changed;
friend class boost::serialization::access;
template <typename Archive>
void serialize(Archive& ar, const unsigned int file_version);

View File

@ -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<HWC>()->InstallAsService(service_manager);
std::make_shared<HWC>(system)->InstallAsService(service_manager);
std::make_shared<RTC>(system)->InstallAsService(service_manager);
}
} // namespace Service::MCU

View File

@ -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<MCU::InfoLedPattern>();
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

View File

@ -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<HWC> {
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)

View File

@ -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 <algorithm>
#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<u8> 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<u32*>(&current_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 <class Archive>
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<s16>(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<u8>(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<u8>(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<u8> result_color{};
s64 arm_time_ns = 0;
friend class boost::serialization::access;
template <class Archive>
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<InfoLedHandler>();
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> RTC::GetService(Core::System& system) {
return system.ServiceManager().GetService<RTC>("mcu::RTC");
}
void RTC::SetInfoLEDPattern(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);
auto pat = rp.PopRaw<InfoLedPattern>();
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<InfoLedPattern::Header>();
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<u8>(GetInfoLEDStatusFinished()));
}
template <class Archive>
void RTC::serialize(Archive& ar, const unsigned int) {
DEBUG_SERIALIZATION_POINT;
ar& boost::serialization::base_object<Kernel::SessionRequestHandler>(*this);
ar & info_led;
ar & info_led_ticking;
}
} // namespace Service::MCU

View File

@ -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<u8, PATTERN_INDEX_COUNT> r{};
std::array<u8, PATTERN_INDEX_COUNT> g{};
std::array<u8, PATTERN_INDEX_COUNT> b{};
friend class boost::serialization::access;
template <class Archive>
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<RTC> {
public:
explicit RTC(Core::System& _system);
~RTC();
void UpdateInfoLEDPattern(const InfoLedPattern& pat);
void UpdateInfoLEDHeader(const InfoLedPattern::Header& header);
bool GetInfoLEDStatusFinished();
static std::shared_ptr<RTC> 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<InfoLedHandler> info_led;
Core::TimingEventType* info_led_tick_event{};
bool info_led_ticking{};
template <class Archive>
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)

View File

@ -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<MCU::InfoLedPattern>();
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<MCU::InfoLedPattern::Header>();
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<u8>(mcu_rtc->GetInfoLEDStatusFinished()));
} else {
rb.Push(ResultUnknown);
rb.Push(u8{});
}
}
void Module::Interface::GetSystemTime(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx);

View File

@ -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:

View File

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