diff --git a/Makefile.common b/Makefile.common index 68b2e6ba5e95..ae9a71c19ff8 100644 --- a/Makefile.common +++ b/Makefile.common @@ -1039,6 +1039,7 @@ endif ifeq ($(HAVE_LAKKA_SWITCH), 1) DEFINES += -DHAVE_LAKKA_SWITCH + OBJ += misc/reboot2payload/reboot2payload.o endif ifeq ($(HAVE_MENU_COMMON), 1) diff --git a/config.def.h b/config.def.h index ac7dde5a5c38..e047bfd64e4a 100644 --- a/config.def.h +++ b/config.def.h @@ -214,6 +214,7 @@ #define DEFAULT_SWITCH_OC false #define DEFAULT_SWITCH_CEC true #define DEFAULT_BLUETOOTH_ERTM false +#define DEFAULT_REBOOT_FORCE_SELF_ON_UPDATE true #endif #if (defined(_WIN32) && !defined(_XBOX)) || (defined(__linux) && !defined(ANDROID) && !defined(HAVE_LAKKA)) || (defined(__MACH__) && !TARGET_OS_IPHONE) || defined(__EMSCRIPTEN__) diff --git a/configuration.c b/configuration.c index 0a66a428bd0c..0f9a2874b30b 100644 --- a/configuration.c +++ b/configuration.c @@ -2054,6 +2054,9 @@ static struct config_array_setting *populate_settings_array( SETTING_ARRAY("cpu_main_gov", settings->arrays.cpu_main_gov, false, NULL, true); SETTING_ARRAY("cpu_menu_gov", settings->arrays.cpu_menu_gov, false, NULL, true); #endif +#ifdef HAVE_LAKKA_SWITCH + SETTING_ARRAY("reboot_payload_name", settings->arrays.reboot_payload_name, false, NULL, true); +#endif *size = count; @@ -2258,6 +2261,7 @@ static struct config_bool_setting *populate_settings_bool( SETTING_BOOL("switch_oc", &settings->bools.switch_oc, true, DEFAULT_SWITCH_OC, false); SETTING_BOOL("switch_cec", &settings->bools.switch_cec, true, DEFAULT_SWITCH_CEC, false); SETTING_BOOL("bluetooth_ertm_disable", &settings->bools.bluetooth_ertm_disable, true, DEFAULT_BLUETOOTH_ERTM, false); + SETTING_BOOL("reboot_force_self_on_update", &settings->bools.reboot_force_self_on_update, true, DEFAULT_REBOOT_FORCE_SELF_ON_UPDATE, false); #endif SETTING_BOOL("audio_rate_control", &settings->bools.audio_rate_control, true, DEFAULT_RATE_CONTROL, false); SETTING_BOOL("audio_enable_menu_ok", &settings->bools.audio_enable_menu_ok, true, DEFAULT_AUDIO_ENABLE_MENU_OK, false); @@ -4398,6 +4402,9 @@ static struct config_uint_setting *populate_settings_uint( SETTING_UINT("cpu_min_freq", &settings->uints.cpu_min_freq, true, 1, false); SETTING_UINT("cpu_max_freq", &settings->uints.cpu_max_freq, true, ~0U, false); #endif +#ifdef HAVE_LAKKA_SWITCH + SETTING_UINT("reboot_payload_kind", &settings->uints.reboot_payload_kind, true, 0, false); +#endif diff --git a/configuration.h b/configuration.h index 8f69a2d2e5d9..58581bdd7141 100644 --- a/configuration.h +++ b/configuration.h @@ -449,6 +449,10 @@ typedef struct settings unsigned cpu_min_freq; unsigned cpu_max_freq; #endif +#ifdef HAVE_LAKKA_SWITCH + /* enum r2p_payload_kind; paired with reboot_payload_name */ + unsigned reboot_payload_kind; +#endif #ifdef HAVE_MIST unsigned steam_rich_presence_format; @@ -1128,6 +1132,10 @@ typedef struct settings bool switch_oc; bool switch_cec; bool bluetooth_ertm_disable; + /* When an update is staged, reboot back into the running + * Lakka entry instead of the selected payload so the + * initramfs installs it. */ + bool reboot_force_self_on_update; #endif #ifdef HAVE_LAKKA bool ssh_enable; @@ -1247,6 +1255,9 @@ typedef struct settings char cpu_main_gov[32]; char cpu_menu_gov[32]; #endif +#ifdef HAVE_LAKKA_SWITCH + char reboot_payload_name[64]; /* R2P_NAME_MAXLEN */ +#endif #ifdef HAVE_MICROPHONE char microphone_driver[32]; char microphone_resampler[32]; diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index f7b2966d9757..bd864d4adaea 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -2494,6 +2494,14 @@ MSG_HASH( MENU_ENUM_LABEL_BLUETOOTH_ERTM_DISABLE, MENU_ENUM_LABEL_BLUETOOTH_ERTM_DISABLE_STR ) +MSG_HASH( + MENU_ENUM_LABEL_REBOOT_PAYLOAD, + MENU_ENUM_LABEL_REBOOT_PAYLOAD_STR + ) +MSG_HASH( + MENU_ENUM_LABEL_REBOOT_FORCE_SELF_ON_UPDATE, + MENU_ENUM_LABEL_REBOOT_FORCE_SELF_ON_UPDATE_STR + ) #endif MSG_HASH( MENU_ENUM_LABEL_SYSTEM_BGM_ENABLE, diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 122eeb50225a..3f7a7aca6249 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -33160,6 +33160,10 @@ MSG_HASH( MSG_VALUE_REBOOTING, "Rebooting..." ) +MSG_HASH( + MSG_VALUE_INSTALL_UPDATE, + "Install Update" + ) MSG_HASH( MSG_VALUE_SHUTTING_DOWN, "Shutting down..." @@ -35574,6 +35578,22 @@ MSG_HASH( MENU_ENUM_SUBLABEL_BLUETOOTH_ERTM_DISABLE, "Disable Bluetooth ERTM to fix pairing of some devices" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_REBOOT_PAYLOAD, + "Reboot Payload" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_REBOOT_PAYLOAD, + "Choose which Hekate entry 'Reboot' in the Main Menu boots into. Use Left/Right to cycle through the entries in hekate_ipl.ini and bootloader/ini/. Defaults to the entry currently running." + ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_REBOOT_FORCE_SELF_ON_UPDATE, + "Force Reboot to Lakka if Update Exists" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_REBOOT_FORCE_SELF_ON_UPDATE, + "When a Lakka update is staged, reboot back into Lakka instead of the selected payload so the update is installed. Disable to always honour the payload selection." + ) #endif MSG_HASH( MSG_LOCALAP_SWITCHING_OFF, diff --git a/lakka.h b/lakka.h index 2e187dc3580b..59ecf717ff7e 100644 --- a/lakka.h +++ b/lakka.h @@ -18,6 +18,9 @@ #ifndef __RARCH_LAKKA_H #define __RARCH_LAKKA_H +#include +#include + #define LAKKA_SSH_PATH "/storage/.cache/services/sshd.conf" #define LAKKA_SAMBA_PATH "/storage/.cache/services/samba.conf" #define LAKKA_SAMBA_DISABLED_FILE_PATH "/storage/.cache/services/samba.disabled" @@ -33,4 +36,18 @@ #define DEFAULT_TIMEZONE "UTC" #define TIMEZONE_LENGTH 255 +RETRO_BEGIN_DECLS + +/* Looks in LAKKA_UPDATE_DIR for an update image the Lakka initramfs + * check_update step installs on the next boot (*.tar, *.img.gz, *.img) + * and keeps the answer for lakka_update_pending(). Reads a directory: + * called when the menu builds the main list and before a reboot, never + * per frame. */ +void lakka_update_check(void); + +/* Whether the last lakka_update_check() found an update staged. */ +bool lakka_update_pending(void); + +RETRO_END_DECLS + #endif diff --git a/menu/cbs/menu_cbs_get_value.c b/menu/cbs/menu_cbs_get_value.c index df677551c826..70842214e515 100644 --- a/menu/cbs/menu_cbs_get_value.c +++ b/menu/cbs/menu_cbs_get_value.c @@ -47,6 +47,12 @@ #include "../../playlist.h" #include "../../manual_content_scan.h" #include "../misc/cpufreq/cpufreq.h" +#ifdef HAVE_LAKKA +#include "../../lakka.h" +#endif +#ifdef HAVE_LAKKA_SWITCH +#include "../../misc/reboot2payload/reboot2payload.h" +#endif #include "../../audio/audio_driver.h" #ifdef HAVE_NETWORKING @@ -691,6 +697,54 @@ static size_t menu_action_cpu_governor_label( MENU_ENUM_LABEL_VALUE_CPU_POLICY_GOVERNOR), len2); return strlcpy(s, d->scaling_governor, len); } + +/* Value column of the main menu "Reboot" entry: shows + * "Install Update" while an update image is staged, so the user + * knows the next reboot will install it. On Switch with pmc_r2p it + * otherwise shows the selected reboot payload (and an update only + * wins when reboot_force_self_on_update is set). Empty otherwise. */ +static size_t menu_action_setting_disp_reboot( + file_list_t* list, + unsigned *w, unsigned type, unsigned i, + const char *label, + char *s, size_t len, + const char *path, + char *s2, size_t len2) +{ + *s = '\0'; + *w = 19; + if (path) + strlcpy(s2, path, len2); +#ifdef HAVE_LAKKA_SWITCH + if (r2p_is_supported()) + { + settings_t *settings = config_get_ptr(); + if ( settings->bools.reboot_force_self_on_update + && lakka_update_pending()) + return strlcpy(s, msg_hash_to_str(MSG_VALUE_INSTALL_UPDATE), len); + return strlcpy(s, r2p_get_destination_name(), len); + } +#endif + if (lakka_update_pending()) + return strlcpy(s, msg_hash_to_str(MSG_VALUE_INSTALL_UPDATE), len); + return 0; +} +#endif + +#ifdef HAVE_LAKKA_SWITCH +static size_t menu_action_setting_disp_reboot_payload( + file_list_t* list, + unsigned *w, unsigned type, unsigned i, + const char *label, + char *s, size_t len, + const char *path, + char *s2, size_t len2) +{ + *w = 19; + if (path) + strlcpy(s2, path, len2); + return strlcpy(s, r2p_get_destination_name(), len); +} #endif static size_t menu_action_setting_disp_set_label_core_lock( @@ -2206,6 +2260,12 @@ static int menu_cbs_init_bind_get_string_representation_compare_label( BIND_ACTION_GET_VALUE(cbs, menu_action_setting_disp_cpu_gov_mode); break; +#ifdef HAVE_LAKKA_SWITCH + case MENU_ENUM_LABEL_REBOOT_PAYLOAD: + BIND_ACTION_GET_VALUE(cbs, + menu_action_setting_disp_reboot_payload); + break; +#endif case MENU_ENUM_LABEL_CPU_POLICY_CORE_GOVERNOR: case MENU_ENUM_LABEL_CPU_POLICY_MENU_GOVERNOR: BIND_ACTION_GET_VALUE(cbs, @@ -2475,12 +2535,16 @@ int menu_cbs_init_bind_get_string_representation(menu_file_list_cbs_t *cbs, #ifdef HAVE_NETWORKING BIND_ACTION_GET_VALUE(cbs, menu_action_setting_disp_set_label_netplay_mitm_server); +#endif + return 0; + case MENU_ENUM_LABEL_REBOOT: +#ifdef HAVE_LAKKA + BIND_ACTION_GET_VALUE(cbs, menu_action_setting_disp_reboot); #endif return 0; case MENU_ENUM_LABEL_RESTART_RETROARCH: case MENU_ENUM_LABEL_QUIT_RETROARCH: case MENU_ENUM_LABEL_SWITCH_GPU_PROFILE: - case MENU_ENUM_LABEL_REBOOT: case MENU_ENUM_LABEL_SHUTDOWN: case MENU_ENUM_LABEL_SAVE_CURRENT_CONFIG: case MENU_ENUM_LABEL_SAVE_NEW_CONFIG: diff --git a/menu/cbs/menu_cbs_left.c b/menu/cbs/menu_cbs_left.c index 90ff2eb9b8a8..9901de9a3ba8 100644 --- a/menu/cbs/menu_cbs_left.c +++ b/menu/cbs/menu_cbs_left.c @@ -48,6 +48,9 @@ #include "../../playlist.h" #include "../../manual_content_scan.h" #include "../misc/cpufreq/cpufreq.h" +#ifdef HAVE_LAKKA_SWITCH +#include "../../misc/reboot2payload/reboot2payload.h" +#endif #ifndef BIND_ACTION_LEFT #define BIND_ACTION_LEFT(cbs, name) (cbs)->action_left = (name) @@ -952,6 +955,17 @@ static int cpu_policy_freq_tweak(unsigned type, const char *label, } #endif +#ifdef HAVE_LAKKA_SWITCH +static int reboot_payload_left(unsigned type, const char *label, + bool wraparound) +{ + struct menu_state *menu_st = menu_state_get_ptr(); + r2p_cycle_selection(config_get_ptr(), false); + menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH; + return 0; +} +#endif + static int core_setting_left(unsigned type, const char *label, bool wraparound) { @@ -1323,6 +1337,11 @@ static int menu_cbs_init_bind_left_compare_label(menu_file_list_cbs_t *cbs, BIND_ACTION_LEFT(cbs, cpu_policy_freq_managed_gov); break; #endif + #ifdef HAVE_LAKKA_SWITCH + case MENU_ENUM_LABEL_REBOOT_PAYLOAD: + BIND_ACTION_LEFT(cbs, reboot_payload_left); + break; + #endif default: return -1; } diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c index 5dfeb6d5ff5e..d22f1814db23 100644 --- a/menu/cbs/menu_cbs_ok.c +++ b/menu/cbs/menu_cbs_ok.c @@ -7087,6 +7087,13 @@ STATIC_DEFAULT_ACTION_OK_FUNC(action_ok_menu_bottom_list, ACTION_OK_DL_MENU_BOTT #endif #ifdef HAVE_LAKKA_SWITCH STATIC_DEFAULT_ACTION_OK_FUNC(action_ok_lakka_switch_options, ACTION_OK_DL_LAKKA_SWITCH_OPTIONS_LIST) + +/* Left/Right cycles the payload selection; OK does nothing. */ +static int action_ok_reboot_payload(const char *path, const char *label, + unsigned type, size_t idx, size_t entry_idx) +{ + return 0; +} #endif STATIC_DEFAULT_ACTION_OK_FUNC(action_ok_rdb_entry, ACTION_OK_DL_RDB_ENTRY) #ifdef HAVE_AUDIOMIXER @@ -9929,6 +9936,7 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs, #endif #ifdef HAVE_LAKKA_SWITCH {MENU_ENUM_LABEL_LAKKA_SWITCH_OPTIONS, action_ok_lakka_switch_options}, + {MENU_ENUM_LABEL_REBOOT_PAYLOAD, action_ok_reboot_payload}, #endif {MENU_ENUM_LABEL_SCREEN_RESOLUTION, action_ok_video_resolution}, {MENU_ENUM_LABEL_PLAYLIST_MANAGER_DEFAULT_CORE, action_ok_playlist_default_core}, diff --git a/menu/cbs/menu_cbs_right.c b/menu/cbs/menu_cbs_right.c index 7becce7d1b91..1a5c490be398 100644 --- a/menu/cbs/menu_cbs_right.c +++ b/menu/cbs/menu_cbs_right.c @@ -49,6 +49,9 @@ #include "../../playlist.h" #include "../../manual_content_scan.h" #include "../misc/cpufreq/cpufreq.h" +#ifdef HAVE_LAKKA_SWITCH +#include "../../misc/reboot2payload/reboot2payload.h" +#endif #ifndef BIND_ACTION_RIGHT #define BIND_ACTION_RIGHT(cbs, name) (cbs)->action_right = (name) @@ -1128,6 +1131,17 @@ static int cpu_policy_freq_tweak(unsigned type, const char *label, } #endif +#ifdef HAVE_LAKKA_SWITCH +static int reboot_payload_right(unsigned type, const char *label, + bool wraparound) +{ + struct menu_state *menu_st = menu_state_get_ptr(); + r2p_cycle_selection(config_get_ptr(), true); + menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH; + return 0; +} +#endif + static int core_setting_right(unsigned type, const char *label, bool wraparound) { @@ -1472,6 +1486,11 @@ static int menu_cbs_init_bind_right_compare_label(menu_file_list_cbs_t *cbs, BIND_ACTION_RIGHT(cbs, cpu_policy_freq_managed_gov); break; #endif + #ifdef HAVE_LAKKA_SWITCH + case MENU_ENUM_LABEL_REBOOT_PAYLOAD: + BIND_ACTION_RIGHT(cbs, reboot_payload_right); + break; + #endif default: return -1; } diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 9b414c52e587..0af75b1f16bc 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -287,6 +287,8 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_switch_options, MENU_ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_switch_oc_enable, MENU_ENUM_SUBLABEL_SWITCH_OC_ENABLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_switch_cec_enable, MENU_ENUM_SUBLABEL_SWITCH_CEC_ENABLE) DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_bluetooth_ertm_disable, MENU_ENUM_SUBLABEL_BLUETOOTH_ERTM_DISABLE) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_reboot_payload, MENU_ENUM_SUBLABEL_REBOOT_PAYLOAD) +DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_reboot_force_self_on_update, MENU_ENUM_SUBLABEL_REBOOT_FORCE_SELF_ON_UPDATE) #endif DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_user_remap_settings, MENU_ENUM_SUBLABEL_INPUT_USER_REMAPS) @@ -3774,6 +3776,12 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_BLUETOOTH_ERTM_DISABLE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_bluetooth_ertm_disable); break; + case MENU_ENUM_LABEL_REBOOT_PAYLOAD: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_reboot_payload); + break; + case MENU_ENUM_LABEL_REBOOT_FORCE_SELF_ON_UPDATE: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_reboot_force_self_on_update); + break; #endif #ifdef HAVE_VIDEO_FILTER case MENU_ENUM_LABEL_VIDEO_FILTER_ENABLE: diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 43ea406484c7..078637ad712c 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -145,6 +145,9 @@ #include "../manual_content_scan.h" #include "../core_backup.h" #include "../misc/cpufreq/cpufreq.h" +#ifdef HAVE_LAKKA_SWITCH +#include "../misc/reboot2payload/reboot2payload.h" +#endif #include "../input/input_remapping.h" #ifdef HAVE_MICROPHONE @@ -12420,21 +12423,37 @@ unsigned menu_displaylist_build_list( #endif #ifdef HAVE_LAKKA_SWITCH case DISPLAYLIST_LAKKA_SWITCH_OPTIONS_LIST: + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_SWITCH_OC_ENABLE, + PARSE_ONLY_BOOL, false) == 0) + count++; + + /* Reboot-to-payload needs the pmc_r2p kernel driver. The + * payload selector has no backing rarch_setting_t; its + * Left/Right/OK/value callbacks live in menu/cbs. */ + if (r2p_is_supported()) { - static const menu_displaylist_build_info_t build_list[] = { - {MENU_ENUM_LABEL_SWITCH_OC_ENABLE, PARSE_ONLY_BOOL}, - {MENU_ENUM_LABEL_SWITCH_CEC_ENABLE, PARSE_ONLY_BOOL}, - {MENU_ENUM_LABEL_BLUETOOTH_ERTM_DISABLE, PARSE_ONLY_BOOL}, - }; + if (menu_entries_append(list, + msg_hash_to_str(MENU_ENUM_LABEL_VALUE_REBOOT_PAYLOAD), + msg_hash_to_str(MENU_ENUM_LABEL_REBOOT_PAYLOAD), + MENU_ENUM_LABEL_REBOOT_PAYLOAD, + MENU_SETTING_ACTION, 0, 0, NULL)) + count++; - for (i = 0; i < ARRAY_SIZE(build_list); i++) - { - if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, - build_list[i].enum_idx, build_list[i].parse_type, - false) == 0) - count++; - } + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_REBOOT_FORCE_SELF_ON_UPDATE, + PARSE_ONLY_BOOL, false) == 0) + count++; } + + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_SWITCH_CEC_ENABLE, + PARSE_ONLY_BOOL, false) == 0) + count++; + if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(list, + MENU_ENUM_LABEL_BLUETOOTH_ERTM_DISABLE, + PARSE_ONLY_BOOL, false) == 0) + count++; break; #endif case DISPLAYLIST_MENU_VIEWS_SETTINGS_LIST: @@ -17538,10 +17557,14 @@ static bool menu_displaylist_ctl_internal( #if defined(HAVE_LAKKA) if (settings->bools.menu_show_reboot) + { + /* What the Reboot entry's value shows. */ + lakka_update_check(); if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(info->list, MENU_ENUM_LABEL_REBOOT, PARSE_ACTION, false) == 0) count++; + } if (settings->bools.menu_show_shutdown) if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(info->list, diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 4950ebce6613..2538a97bffa3 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -18182,6 +18182,21 @@ static void settings_build_lakka_switch_options( general_read_handler, SD_FLAG_NONE); SETTINGS_ACTION_SET(change, &(*list)[list_info->index - 1], bluetooth_ertm_disable_toggle_change_handler) + + CONFIG_BOOL( + list, list_info, + &settings->bools.reboot_force_self_on_update, + MENU_ENUM_LABEL_REBOOT_FORCE_SELF_ON_UPDATE, + MENU_ENUM_LABEL_VALUE_REBOOT_FORCE_SELF_ON_UPDATE, + DEFAULT_REBOOT_FORCE_SELF_ON_UPDATE, + MENU_ENUM_LABEL_VALUE_OFF, + MENU_ENUM_LABEL_VALUE_ON, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler, + SD_FLAG_NONE); GROUP_END(); } diff --git a/misc/reboot2payload/reboot2payload.c b/misc/reboot2payload/reboot2payload.c new file mode 100644 index 000000000000..2e48dd47a27e --- /dev/null +++ b/misc/reboot2payload/reboot2payload.c @@ -0,0 +1,424 @@ +/* RetroArch - A frontend for libretro. + * Copyright (C) 2026 - gavin_darkglider + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "reboot2payload.h" +#include "../../configuration.h" +#include "../../verbosity.h" + +#define R2P_SYSFS_DIR "/sys/devices/r2p" +#define R2P_SYSFS_ACTION R2P_SYSFS_DIR "/action" +#define R2P_SYSFS_PARAM1 R2P_SYSFS_DIR "/param1" +#define R2P_SYSFS_PARAM2 R2P_SYSFS_DIR "/param2" + +/* Lakka's initramfs always mounts the FAT boot partition at /flash. */ +#define R2P_BOOT_ROOT "/flash" +#define R2P_HEKATE_IPL R2P_BOOT_ROOT "/bootloader/hekate_ipl.ini" +#define R2P_HEKATE_INIDIR R2P_BOOT_ROOT "/bootloader/ini" + +#define R2P_CMDLINE_PATH "/proc/cmdline" +#define R2P_DEFAULT_ACTION "self" +#define R2P_SELF_SUFFIX " (self)" +#define R2P_HEKATE_MENU "Hekate Menu" + +enum r2p_payload_kind +{ + R2P_KIND_BOOTLOADER = 0, /* action=bootloader */ + R2P_KIND_LAUNCH, /* action=self, param2=0 (ipl.ini) */ + R2P_KIND_INI_CONFIG /* action=self, param2=1 (ini/) */ +}; + +typedef struct r2p_payload +{ + char name[R2P_NAME_MAXLEN]; + enum r2p_payload_kind kind; + unsigned index; +} r2p_payload_t; + +static r2p_payload_t *r2p_list = NULL; +static size_t r2p_list_count = 0; +static size_t r2p_list_capacity = 0; +static size_t r2p_selected = 0; +static bool r2p_list_initialized = false; + +/* pmc_r2p.* values from /proc/cmdline: the entry Lakka was booted + * from. Seeds the default selection and feeds r2p_arm_self(). */ +static char r2p_self_action[16] = R2P_DEFAULT_ACTION; +static unsigned r2p_self_param1 = 0; +static unsigned r2p_self_param2 = 0; + +static bool r2p_append_payload(const char *name, + enum r2p_payload_kind kind, unsigned index) +{ + r2p_payload_t *slot; + + if (string_is_empty(name)) + return false; + + if (r2p_list_count >= r2p_list_capacity) + { + size_t new_cap = (r2p_list_capacity == 0) + ? 8 : r2p_list_capacity * 2; + r2p_payload_t *grown = (r2p_payload_t*)realloc(r2p_list, + new_cap * sizeof(*grown)); + if (!grown) + return false; + r2p_list = grown; + r2p_list_capacity = new_cap; + } + + slot = &r2p_list[r2p_list_count++]; + strlcpy(slot->name, name, sizeof(slot->name)); + slot->kind = kind; + slot->index = index; + return true; +} + +static char *r2p_trim(char *s) +{ + char *end; + while (*s && isspace((unsigned char)*s)) + s++; + end = s + strlen(s); + while (end > s && isspace((unsigned char)end[-1])) + end--; + *end = '\0'; + return s; +} + +static void r2p_parse_cmdline(void) +{ + void *buf = NULL; + int64_t _len = 0; + char *line; + char *tok; + char *saveptr = NULL; + + strlcpy(r2p_self_action, R2P_DEFAULT_ACTION, sizeof(r2p_self_action)); + r2p_self_param1 = 0; + r2p_self_param2 = 0; + + /* The whole of it: a command line runs to several KiB, and the + * pmc_r2p.* values Hekate appends come last. */ + if (!filestream_read_file(R2P_CMDLINE_PATH, &buf, &_len) || !buf) + return; + line = (char*)buf; + + for (tok = strtok_r(line, " \t\r\n", &saveptr); + tok; + tok = strtok_r(NULL, " \t\r\n", &saveptr)) + { + if (string_starts_with(tok, "pmc_r2p.action=")) + strlcpy(r2p_self_action, tok + STRLEN_CONST("pmc_r2p.action="), + sizeof(r2p_self_action)); + else if (string_starts_with(tok, "pmc_r2p.param1=")) + r2p_self_param1 = (unsigned)strtoul( + tok + STRLEN_CONST("pmc_r2p.param1="), NULL, 0); + else if (string_starts_with(tok, "pmc_r2p.param2=")) + r2p_self_param2 = (unsigned)strtoul( + tok + STRLEN_CONST("pmc_r2p.param2="), NULL, 0); + } + free(buf); +} + +/* Appends one entry per [section] of a Hekate INI, numbering them + * from *index_inout the same way Hekate does. */ +static void r2p_parse_ini_file(const char *ini_path, + enum r2p_payload_kind kind, unsigned *index_inout) +{ + char line[256]; + RFILE *fp = filestream_open(ini_path, + RETRO_VFS_FILE_ACCESS_READ, + RETRO_VFS_FILE_ACCESS_HINT_NONE); + + if (!fp) + return; + + while (filestream_gets(fp, line, sizeof(line))) + { + char *section = r2p_trim(line); + size_t _len = strlen(section); + + if (_len < 2 || section[0] != '[' || section[_len - 1] != ']') + continue; + + section[_len - 1] = '\0'; + section = r2p_trim(section + 1); + + /* [config] holds Hekate's global options, not a boot entry. */ + if ( string_is_empty(section) + || string_is_equal_case_insensitive(section, "config")) + continue; + + r2p_append_payload(section, kind, *index_inout); + (*index_inout)++; + } + + filestream_close(fp); +} + +static void r2p_clear_payloads(void) +{ + free(r2p_list); + r2p_list = NULL; + r2p_list_count = 0; + r2p_list_capacity = 0; +} + +/* List index of the entry Lakka was booted from, or 0 (Hekate menu) + * when no parsed entry matches the kernel command line. */ +static size_t r2p_index_for_self(void) +{ + size_t i; + enum r2p_payload_kind want_kind; + + if (!string_is_equal_case_insensitive(r2p_self_action, "self")) + return 0; + + want_kind = (r2p_self_param2 == 1) + ? R2P_KIND_INI_CONFIG + : R2P_KIND_LAUNCH; + + for (i = 0; i < r2p_list_count; i++) + if ( r2p_list[i].kind == want_kind + && r2p_list[i].index == r2p_self_param1) + return i; + + return 0; +} + +static void r2p_refresh_payloads(void) +{ + unsigned launch_index = 1; + unsigned cfg_index = 1; + + r2p_clear_payloads(); + r2p_parse_cmdline(); + + r2p_append_payload(R2P_HEKATE_MENU, R2P_KIND_BOOTLOADER, 0); + + if (path_is_valid(R2P_HEKATE_IPL)) + r2p_parse_ini_file(R2P_HEKATE_IPL, R2P_KIND_LAUNCH, &launch_index); + + if (path_is_directory(R2P_HEKATE_INIDIR)) + { + struct string_list *ini_files = dir_list_new(R2P_HEKATE_INIDIR, + "ini", false, false, false, false); + if (ini_files) + { + size_t i; + dir_list_sort(ini_files, true); + for (i = 0; i < ini_files->size; i++) + r2p_parse_ini_file(ini_files->elems[i].data, + R2P_KIND_INI_CONFIG, &cfg_index); + string_list_free(ini_files); + } + } + + r2p_selected = r2p_index_for_self(); + r2p_list_initialized = true; + + RARCH_LOG("[R2P] %u entries loaded; cmdline action=%s " + "param1=%u param2=%u, default idx=%u.\n", + (unsigned)r2p_list_count, r2p_self_action, + r2p_self_param1, r2p_self_param2, + (unsigned)r2p_selected); +} + +static void r2p_ensure_initialized(void) +{ + if (!r2p_list_initialized) + r2p_refresh_payloads(); +} + +bool r2p_is_supported(void) +{ + /* The kernel's: it does not come or go while running, and the menu + * asks every frame. */ + static int supported = -1; + if (supported < 0) + supported = path_is_directory(R2P_SYSFS_DIR) ? 1 : 0; + return supported == 1; +} + +void r2p_load_selection(const struct settings *settings) +{ + size_t i; + unsigned saved_kind; + const char *saved_name; + + r2p_refresh_payloads(); + + if (!settings) + return; + + saved_kind = settings->uints.reboot_payload_kind; + saved_name = settings->arrays.reboot_payload_name; + + if (string_is_empty(saved_name)) + return; + + for (i = 0; i < r2p_list_count; i++) + { + if ( (unsigned)r2p_list[i].kind == saved_kind + && string_is_equal(r2p_list[i].name, saved_name)) + { + r2p_selected = i; + return; + } + } + + RARCH_WARN("[R2P] Saved payload \"%s\" not found, " + "using the running entry.\n", saved_name); +} + +void r2p_cycle_selection(struct settings *settings, bool forward) +{ + const r2p_payload_t *p; + + r2p_ensure_initialized(); + + if (r2p_list_count == 0) + return; + + if (forward) + r2p_selected = (r2p_selected + 1) % r2p_list_count; + else + r2p_selected = (r2p_selected == 0) + ? r2p_list_count - 1 + : r2p_selected - 1; + + if (!settings) + return; + + p = &r2p_list[r2p_selected]; + settings->uints.reboot_payload_kind = (unsigned)p->kind; + strlcpy(settings->arrays.reboot_payload_name, p->name, + sizeof(settings->arrays.reboot_payload_name)); +} + +const char *r2p_get_destination_name(void) +{ + static char buf[R2P_NAME_MAXLEN + sizeof(R2P_SELF_SUFFIX)]; + const r2p_payload_t *p; + + r2p_ensure_initialized(); + + if (r2p_selected >= r2p_list_count) + return R2P_HEKATE_MENU; + + p = &r2p_list[r2p_selected]; + if (r2p_selected != r2p_index_for_self()) + return p->name; + + snprintf(buf, sizeof(buf), "%s%s", p->name, R2P_SELF_SUFFIX); + return buf; +} + +static bool r2p_write_sysfs(const char *action, + const char *param1, const char *param2) +{ + if (!filestream_write_file(R2P_SYSFS_ACTION, action, strlen(action))) + { + RARCH_ERR("[R2P] Failed to write \"%s\".\n", R2P_SYSFS_ACTION); + return false; + } + if (!filestream_write_file(R2P_SYSFS_PARAM1, param1, strlen(param1))) + { + RARCH_ERR("[R2P] Failed to write \"%s\".\n", R2P_SYSFS_PARAM1); + return false; + } + if (!filestream_write_file(R2P_SYSFS_PARAM2, param2, strlen(param2))) + { + RARCH_ERR("[R2P] Failed to write \"%s\".\n", R2P_SYSFS_PARAM2); + return false; + } + return true; +} + +bool r2p_arm_selected(void) +{ + const r2p_payload_t *p; + const char *action = "self"; + char param1[16]; + const char *param2 = "0"; + + r2p_ensure_initialized(); + + if (r2p_selected >= r2p_list_count) + return false; + + p = &r2p_list[r2p_selected]; + snprintf(param1, sizeof(param1), "%u", p->index); + + switch (p->kind) + { + case R2P_KIND_BOOTLOADER: + action = "bootloader"; + break; + case R2P_KIND_LAUNCH: + break; + case R2P_KIND_INI_CONFIG: + param2 = "1"; + break; + default: + return false; + } + + if (!r2p_write_sysfs(action, param1, param2)) + return false; + + RARCH_LOG("[R2P] Armed action=%s param1=%s param2=%s (\"%s\").\n", + action, param1, param2, p->name); + return true; +} + +bool r2p_arm_self(void) +{ + char param1[16]; + char param2[16]; + + r2p_ensure_initialized(); + + snprintf(param1, sizeof(param1), "%u", r2p_self_param1); + snprintf(param2, sizeof(param2), "%u", r2p_self_param2); + + if (!r2p_write_sysfs(r2p_self_action, param1, param2)) + return false; + + RARCH_LOG("[R2P] Armed self action=%s param1=%s param2=%s.\n", + r2p_self_action, param1, param2); + return true; +} + +void r2p_deinit(void) +{ + r2p_clear_payloads(); + r2p_selected = 0; + r2p_list_initialized = false; +} diff --git a/misc/reboot2payload/reboot2payload.h b/misc/reboot2payload/reboot2payload.h new file mode 100644 index 000000000000..d3c7046b36f4 --- /dev/null +++ b/misc/reboot2payload/reboot2payload.h @@ -0,0 +1,76 @@ +/* RetroArch - A frontend for libretro. + * Copyright (C) 2026 - gavin_darkglider + * + * RetroArch is free software: you can redistribute it and/or modify it under the terms + * of the GNU General Public License as published by the Free Software Found- + * ation, either version 3 of the License, or (at your option) any later version. + * + * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with RetroArch. + * If not, see . + */ + +#ifndef __RARCH_REBOOT2PAYLOAD_H +#define __RARCH_REBOOT2PAYLOAD_H + +#include + +#include +#include + +RETRO_BEGIN_DECLS + +/* Reboot-to-payload support for Lakka on the Nintendo Switch. + * + * The Switchroot L4T kernel exposes the pmc_r2p driver at + * /sys/devices/r2p. Writing action/param1/param2 there selects what + * Hekate boots on the next reboot: + * + * action=bootloader -> stop in the Hekate menu + * action=self, param2=0, param1=N -> Nth [entry] of hekate_ipl.ini + * action=self, param2=1, param1=N -> Nth [entry] across + * bootloader/ini/ (sorted) + * + * The list of destinations is parsed from the Hekate configuration on + * the FAT boot partition (mounted at /flash by Lakka). The default + * selection is the entry Lakka was booted from, taken from the + * pmc_r2p.* parameters on the kernel command line. */ + +#define R2P_NAME_MAXLEN 64 + +struct settings; + +/* True when the running kernel provides the pmc_r2p sysfs interface. */ +bool r2p_is_supported(void); + +/* Re-parses the payload list, then restores the selection persisted in + * settings (reboot_payload_kind / reboot_payload_name). The persisted + * entry is matched by kind and name, so it survives INI reordering; + * if it no longer exists the selection falls back to the entry Lakka + * was booted from. */ +void r2p_load_selection(const struct settings *settings); + +/* Steps the selection forward or backward (wrapping) and mirrors the + * new selection into settings so it is saved with the config. */ +void r2p_cycle_selection(struct settings *settings, bool forward); + +/* Display name of the selected destination. The entry Lakka was + * booted from is suffixed with " (self)". */ +const char *r2p_get_destination_name(void); + +/* Arms pmc_r2p for the selected destination. */ +bool r2p_arm_selected(void); + +/* Arms pmc_r2p with the pmc_r2p.* values from the kernel command + * line, i.e. reboots back into the running Lakka entry regardless + * of the selection. */ +bool r2p_arm_self(void); + +void r2p_deinit(void); + +RETRO_END_DECLS + +#endif diff --git a/msg_hash.h b/msg_hash.h index 1bef71b7ffe8..d391469217b4 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -373,6 +373,7 @@ enum msg_hash_enums MSG_FILE_NOT_FOUND, MSG_VALUE_SHUTTING_DOWN, MSG_VALUE_REBOOTING, + MSG_VALUE_INSTALL_UPDATE, MSG_FAILED_TO_START_AUDIO_DRIVER, MSG_FOUND_LAST_STATE_SLOT, MSG_FOUND_LAST_REPLAY_SLOT, @@ -22570,6 +22571,8 @@ enum msg_hash_enums MENU_LABEL(SWITCH_OC_ENABLE), MENU_LABEL(SWITCH_CEC_ENABLE), MENU_LABEL(BLUETOOTH_ERTM_DISABLE), + MENU_LABEL(REBOOT_PAYLOAD), + MENU_LABEL(REBOOT_FORCE_SELF_ON_UPDATE), #endif MENU_LBL_H(NETPLAY_START_AS_SPECTATOR), MENU_LABEL(NETPLAY_FADE_CHAT), diff --git a/msg_hash_lbl_str.h b/msg_hash_lbl_str.h index f844558bb919..f4eaddf0e53b 100644 --- a/msg_hash_lbl_str.h +++ b/msg_hash_lbl_str.h @@ -625,6 +625,8 @@ #define MENU_ENUM_LABEL_SWITCH_OC_ENABLE_STR "Switch_OC_enable" #define MENU_ENUM_LABEL_SWITCH_CEC_ENABLE_STR "Switch_CEC_enable" #define MENU_ENUM_LABEL_BLUETOOTH_ERTM_DISABLE_STR "Switch_ERTM_Disable" +#define MENU_ENUM_LABEL_REBOOT_PAYLOAD_STR "reboot_payload" +#define MENU_ENUM_LABEL_REBOOT_FORCE_SELF_ON_UPDATE_STR "reboot_force_self_on_update" #define MENU_ENUM_LABEL_SYSTEM_BGM_ENABLE_STR "system_bgm_enable" #define MENU_ENUM_LABEL_SYSTEM_INFORMATION_STR "system_information" #define MENU_ENUM_LABEL_SYSTEM_INFO_ENTRY_STR "system_info_entry" diff --git a/retroarch.c b/retroarch.c index 93d375b3349a..0cb530efa4d7 100644 --- a/retroarch.c +++ b/retroarch.c @@ -269,12 +269,50 @@ #endif #ifdef HAVE_LAKKA +#include #include "lakka.h" +#ifdef HAVE_LAKKA_SWITCH +#include "misc/reboot2payload/reboot2payload.h" +#endif #include #endif #define _PSUPP(var, name, desc) printf(" %s:\n\t\t%s: %s\n", name, desc, var ? "yes" : "no") +#ifdef HAVE_LAKKA +static bool lakka_update_staged = false; + +bool lakka_update_pending(void) +{ + return lakka_update_staged; +} + +void lakka_update_check(void) +{ + bool found = false; + struct RDIR *dir = retro_opendir(LAKKA_UPDATE_DIR); + + lakka_update_staged = false; + if (!dir) + return; + + while (!found && retro_readdir(dir)) + { + const char *name = retro_dirent_get_name(dir); + + if (retro_dirent_is_dir(dir, NULL)) + continue; + + found = string_ends_with(name, ".tar") + || string_ends_with(name, ".img.gz") + || string_ends_with(name, ".img"); + } + + retro_closedir(dir); + lakka_update_staged = found; +} +#endif + #define FAIL_CPU(simd_type) do { \ RARCH_ERR(simd_type " code is compiled in, but CPU does not support this feature. Cannot continue.\n"); \ retroarch_fail(1, "validate_cpu_features()"); \ @@ -1916,6 +1954,13 @@ void drivers_init( gfx_display_init_first_driver(p_disp, video_is_threaded); } +#ifdef HAVE_LAKKA_SWITCH + /* Parse the Hekate boot entries and restore the saved reboot + * payload before the menu driver is initialised, so the first + * menu build already shows the right destination. */ + r2p_load_selection(settings); +#endif + #ifdef HAVE_MENU if (flags & DRIVER_VIDEO_MASK) { @@ -2144,6 +2189,9 @@ void driver_uninit(int flags, enum driver_lifetime_flags lifetime_flags) #ifdef HAVE_LAKKA cpu_scaling_driver_free(); #endif +#ifdef HAVE_LAKKA_SWITCH + r2p_deinit(); +#endif } static void retroarch_deinit_drivers(struct retro_callbacks *cbs) @@ -5154,6 +5202,21 @@ bool command_event(enum event_command cmd, void *data) { #if defined(__linux__) && !defined(ANDROID) const char *_msg = msg_hash_to_str(MSG_VALUE_REBOOTING); +#ifdef HAVE_LAKKA_SWITCH + /* Tell Hekate what to boot next via pmc_r2p. A staged + * update reboots back into the running Lakka entry + * (unless the user opted out) so the initramfs installs + * it. If arming fails this is a plain reboot. */ + if (r2p_is_supported()) + { + lakka_update_check(); + if ( settings->bools.reboot_force_self_on_update + && lakka_update_pending()) + r2p_arm_self(); + else + r2p_arm_selected(); + } +#endif if (settings->bools.config_save_on_exit) command_event(CMD_EVENT_MENU_SAVE_CURRENT_CONFIG, NULL); runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL,