/* * OmniNX Installer Payload * Minimal payload to install OmniNX CFW Pack files outside of Horizon OS * * Based on HATS Installer by sthetix * Based on TegraExplorer/hekate by CTCaer, naehrwert, shchmue */ #ifndef VERSION #define VERSION "1.0.0" #endif #include #include #include "gfx.h" #include #include #include #include #include #include #include #include #include #include #include #include "nx_sd.h" #include #include #include #include #include #include "fs.h" #include "version.h" #include "install.h" #include "screenshot.h" // Configuration #define PAYLOAD_PATH "sd:/bootloader/update.bin" // Payload launch defines #define RELOC_META_OFF 0x7C #define PATCHED_RELOC_SZ 0x94 #define PATCHED_RELOC_STACK 0x40007000 #define PATCHED_RELOC_ENTRY 0x40010000 #define EXT_PAYLOAD_ADDR 0xC0000000 #define RCM_PAYLOAD_ADDR (EXT_PAYLOAD_ADDR + ALIGN(PATCHED_RELOC_SZ, 0x10)) #define COREBOOT_END_ADDR 0xD0000000 // Hekate config structure (simplified) typedef struct _hekate_config { u32 autoboot; u32 autoboot_list; u32 bootwait; u32 backlight; u32 autohosoff; u32 autonogc; u32 updater2p; u32 bootprotect; bool t210b01; bool se_keygen_done; bool sept_run; bool aes_slots_new; bool emummc_force_disable; bool rcm_patched; u32 errors; } hekate_config; // Boot config structures (required by bdk) hekate_config h_cfg; boot_cfg_t __attribute__((section ("._boot_cfg"))) b_cfg; volatile nyx_storage_t *nyx_str = (nyx_storage_t *)NYX_STORAGE_ADDR; static void *coreboot_addr; static int total_errors = 0; // Use BDK colors (CYAN/WHITE overridden for consistency; GREEN/YELLOW from types.h) #undef COLOR_CYAN #undef COLOR_WHITE #define COLOR_CYAN 0xFF00FFFF #define COLOR_WHITE 0xFFFFFFFF static void set_default_configuration(void) { h_cfg.autoboot = 0; h_cfg.autoboot_list = 0; h_cfg.bootwait = 3; h_cfg.se_keygen_done = 0; h_cfg.backlight = 100; h_cfg.autohosoff = 0; h_cfg.autonogc = 1; h_cfg.updater2p = 0; h_cfg.bootprotect = 0; h_cfg.errors = 0; h_cfg.sept_run = 0; h_cfg.aes_slots_new = false; h_cfg.rcm_patched = fuse_check_patched_rcm(); h_cfg.emummc_force_disable = false; h_cfg.t210b01 = hw_get_chip_id() == GP_HIDREV_MAJOR_T210B01; } static void set_color(u32 color) { gfx_con_setcol(color, gfx_con.fillbg, gfx_con.bgcol); } static void print_header(void) { gfx_clear_grey(0x1B); gfx_con_setpos(0, 0); set_color(COLOR_CYAN); gfx_printf("===================================\n"); gfx_printf(" OmniNX Installer Payload v%s\n", VERSION); gfx_printf("===================================\n\n"); set_color(COLOR_WHITE); } /* Console Vol+ and Vol- together (exit / cancel where documented) */ static inline bool cancel_combo_pressed(u8 btn) { return (btn & (BTN_VOL_UP | BTN_VOL_DOWN)) == (BTN_VOL_UP | BTN_VOL_DOWN); } void reloc_patcher(u32 payload_dst, u32 payload_src, u32 payload_size) { memcpy((u8 *)payload_src, (u8 *)IPL_LOAD_ADDR, PATCHED_RELOC_SZ); volatile reloc_meta_t *relocator = (reloc_meta_t *)(payload_src + RELOC_META_OFF); relocator->start = payload_dst - ALIGN(PATCHED_RELOC_SZ, 0x10); relocator->stack = PATCHED_RELOC_STACK; relocator->end = payload_dst + payload_size; relocator->ep = payload_dst; if (payload_size == 0x7000) { memcpy((u8 *)(payload_src + ALIGN(PATCHED_RELOC_SZ, 0x10)), coreboot_addr, 0x7000); } } static int launch_payload(const char *path) { if (!path) return 1; if (sd_mount()) { FIL fp; if (f_open(&fp, path, FA_READ)) { gfx_printf("Payload nicht gefunden: %s\n", path); return 1; } void *buf; u32 size = f_size(&fp); if (size < 0x30000) buf = (void *)RCM_PAYLOAD_ADDR; else { coreboot_addr = (void *)(COREBOOT_END_ADDR - size); buf = coreboot_addr; } if (f_read(&fp, buf, size, NULL)) { f_close(&fp); return 1; } f_close(&fp); sd_unmount(); if (size < 0x30000) { reloc_patcher(PATCHED_RELOC_ENTRY, EXT_PAYLOAD_ADDR, ALIGN(size, 0x10)); hw_reinit_workaround(false, byte_swap_32(*(u32 *)(buf + size - sizeof(u32)))); } else { reloc_patcher(PATCHED_RELOC_ENTRY, EXT_PAYLOAD_ADDR, 0x7000); hw_reinit_workaround(true, 0); } sdmmc_storage_init_wait_sd(); void (*ext_payload_ptr)() = (void *)EXT_PAYLOAD_ADDR; (*ext_payload_ptr)(); } return 1; } static int file_exists(const char *path) { FILINFO fno; return (f_stat(path, &fno) == FR_OK); } extern void pivot_stack(u32 stack_top); void ipl_main(void) { // Hardware initialization hw_init(); pivot_stack(IPL_STACK_TOP); heap_init(IPL_HEAP_START); // Set bootloader's default configuration set_default_configuration(); // Mount SD Card if (!sd_mount()) { // Can't show error without display, just reboot power_set_state(POWER_OFF_REBOOT); } // Initialize minerva for faster memory minerva_init(); minerva_change_freq(FREQ_800); // Initialize display display_init(); u32 *fb = display_init_framebuffer_pitch(); gfx_init_ctxt(fb, 720, 1280, 720); gfx_con_init(); display_backlight_pwm_init(); display_backlight_brightness(100, 1000); // Overclock BPMP bpmp_clk_rate_set(BPMP_CLK_DEFAULT_BOOST); print_header(); // Detect current OmniNX installation omninx_status_t current = detect_omninx_installation(); // Detect which pack variant(s) are on SD card omninx_variant_t variants_present[3]; int num_variants = detect_present_variants(variants_present, 3); if (num_variants == 0) { set_color(COLOR_RED); gfx_printf("FEHLER: Kein OmniNX-Paket auf der SD-Karte gefunden!\n"); gfx_printf("Erwartet wird eines der folgenden:\n"); gfx_printf(" - sd:/OmniNX Standard/\n"); gfx_printf(" - sd:/OmniNX Light/\n"); gfx_printf(" - sd:/OmniNX OC/\n\n"); set_color(COLOR_WHITE); // Initialize joycons for button input jc_init_hw(); // Wait for button confirmation bool button_pressed = false; // Launch payload if available if (file_exists(PAYLOAD_PATH)) { set_color(COLOR_GREEN); gfx_printf("Druecke A-Taste (rechter Joy-Con) oder Power-Taste,\n"); gfx_printf("um Hekate zu starten...\n"); gfx_printf("Oder Vol+ und Vol- (Konsole) gleichzeitig.\n"); set_color(COLOR_WHITE); } else { set_color(COLOR_GREEN); gfx_printf("Druecke A-Taste (rechter Joy-Con) oder Power-Taste,\n"); gfx_printf("um den Neustart zu starten...\n"); gfx_printf("Oder Vol+ und Vol- (Konsole) gleichzeitig.\n"); set_color(COLOR_WHITE); } // First, wait for power button to be released if it's currently pressed while (btn_read() & BTN_POWER) { msleep(50); } while (!button_pressed) { // Check power button u8 btn_state = btn_read(); if (btn_state & BTN_POWER) { button_pressed = true; break; } // Check joycon A button; Capture = screenshot jc_gamepad_rpt_t *jc = joycon_poll(); if (jc) { if (jc->cap) take_screenshot(); if (jc->a) { button_pressed = true; break; } } if (cancel_combo_pressed(btn_state)) { button_pressed = true; break; } msleep(50); // Small delay to avoid busy-waiting } // Launch payload if available, otherwise reboot if (file_exists(PAYLOAD_PATH)) { gfx_printf("\n"); set_color(COLOR_CYAN); gfx_printf("Payload wird gestartet...\n"); set_color(COLOR_WHITE); msleep(500); launch_payload(PAYLOAD_PATH); } else { power_set_state(POWER_OFF_REBOOT); } return; } // If multiple variants present, show selection menu; otherwise use the single one omninx_variant_t pack_variant; if (num_variants == 1) { pack_variant = variants_present[0]; } else { // Initialize joycons for menu input jc_init_hw(); while (btn_read() & BTN_POWER) { msleep(50); } // Selection menu (TegraExplorer-style: draw once, then only redraw changed lines) int selected = 0; int prev_selected = 0; bool confirmed = false; // Draw menu once and remember start position of variant lines gfx_clear_grey(0x1B); gfx_con_setpos(0, 0); print_header(); set_color(COLOR_YELLOW); gfx_printf("Mehrere OmniNX-Pakete gefunden.\n"); gfx_printf("Waehle die zu installierende Variante:\n\n"); set_color(COLOR_WHITE); u32 menu_x, menu_variant_start_y; gfx_con_getpos(&menu_x, &menu_variant_start_y); for (int i = 0; i < num_variants; i++) { if (i == selected) { set_color(COLOR_GREEN); gfx_printf(" > %s\n", get_variant_name(variants_present[i])); set_color(COLOR_WHITE); } else { gfx_printf(" %s\n", get_variant_name(variants_present[i])); } } gfx_printf("\n"); set_color(COLOR_CYAN); gfx_printf("D-Pad / Vol+/-: Auswahl | A oder Power: Bestaetigen\n"); gfx_printf("Vol+ und Vol- gleichzeitig: Abbrechen (Hekate)\n"); set_color(COLOR_WHITE); // Edge detection: only move on press (not while held) bool prev_up = false, prev_down = false; bool menu_aborted = false; while (!confirmed && !menu_aborted) { // On selection change: redraw only the two affected lines (no full clear) if (selected != prev_selected) { gfx_con_setpos(menu_x, menu_variant_start_y + (u32)prev_selected * 16); set_color(COLOR_WHITE); gfx_printf(" %s\n", get_variant_name(variants_present[prev_selected])); gfx_con_setpos(menu_x, menu_variant_start_y + (u32)selected * 16); set_color(COLOR_GREEN); gfx_printf(" > %s\n", get_variant_name(variants_present[selected])); set_color(COLOR_WHITE); prev_selected = selected; } jc_gamepad_rpt_t *jc = joycon_poll(); u8 btn = btn_read(); if (jc && jc->cap) take_screenshot(); if (cancel_combo_pressed(btn)) { menu_aborted = true; break; } // D-pad or Vol+ / Vol- for selection (Vol+ = up, Vol- = down) bool cur_up = false, cur_down = false; if (jc) { cur_up = (jc->up != 0) && !jc->down; cur_down = (jc->down != 0) && !jc->up; } if (btn & BTN_VOL_UP) cur_up = true; if (btn & BTN_VOL_DOWN) cur_down = true; if (cur_up && !prev_up) selected = (selected - 1 + num_variants) % num_variants; else if (cur_down && !prev_down) selected = (selected + 1) % num_variants; else if (jc && jc->a) confirmed = true; prev_up = cur_up; prev_down = cur_down; if (btn & BTN_POWER) { confirmed = true; } msleep(50); } if (menu_aborted) { gfx_printf("\n"); set_color(COLOR_YELLOW); gfx_printf("Abgebrochen. Starte Hekate...\n"); set_color(COLOR_WHITE); msleep(500); if (file_exists(PAYLOAD_PATH)) { launch_payload(PAYLOAD_PATH); } else { power_set_state(POWER_OFF_REBOOT); } return; } pack_variant = variants_present[selected]; gfx_clear_grey(0x1B); print_header(); } // Determine installation mode install_mode_t mode = current.is_installed ? INSTALL_MODE_UPDATE : INSTALL_MODE_CLEAN; // Battery run protection: below 10% requires charger #define BATT_LOW_THRESHOLD 10 int batt_raw = 0; int batt_pct = 100; if (max17050_get_property(MAX17050_RepSOC, &batt_raw) == 0) { batt_pct = batt_raw >> 8; // RepSOC: high byte = percent } while (batt_pct < BATT_LOW_THRESHOLD && !bq24193_charger_connected()) { gfx_clear_grey(0x1B); gfx_con_setpos(0, 0); print_header(); set_color(COLOR_RED); gfx_printf("Akkustand zu niedrig! (%d%%)\n\n", batt_pct); gfx_printf("Unter %d%% Akku darf die Installation nur\n", BATT_LOW_THRESHOLD); gfx_printf("mit angeschlossenem Ladegeraet durchgefuehrt werden.\n\n"); set_color(COLOR_YELLOW); gfx_printf("Stecke das Ladegeraet an und warte...\n"); gfx_printf("Oder Vol+ und Vol- (Konsole) zum Abbrechen.\n"); set_color(COLOR_WHITE); jc_init_hw(); while (btn_read() & BTN_POWER) { msleep(50); } bool user_cancelled = false; while (batt_pct < BATT_LOW_THRESHOLD && !bq24193_charger_connected() && !user_cancelled) { u8 pbtn = btn_read(); jc_gamepad_rpt_t *jc = joycon_poll(); if (jc) { if (jc->cap) take_screenshot(); } if (cancel_combo_pressed(pbtn)) { user_cancelled = true; break; } if (max17050_get_property(MAX17050_RepSOC, &batt_raw) == 0) { batt_pct = batt_raw >> 8; } msleep(200); } if (user_cancelled) { gfx_printf("\nAbgebrochen. Starte Hekate...\n"); msleep(500); if (file_exists(PAYLOAD_PATH)) { launch_payload(PAYLOAD_PATH); } else { power_set_state(POWER_OFF_REBOOT); } return; } } // Charger detected or battery OK - clear the low-battery screen before continuing gfx_clear_grey(0x1B); gfx_con_setpos(0, 0); print_header(); // UHS class check: recommend U2 / V30 / A2 for emuMMC (speed and reliability) #define UHS_U2_MIN 2 #define UHS_V30_MIN 30 #define UHS_A2_MIN 2 bool uhs_ok = (sd_storage.ssr.uhs_grade >= UHS_U2_MIN && sd_storage.ssr.video_class >= UHS_V30_MIN && sd_storage.ssr.app_class >= UHS_A2_MIN); if (!uhs_ok) { jc_init_hw(); gfx_clear_grey(0x1B); gfx_con_setpos(0, 0); set_color(COLOR_RED); gfx_printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"); gfx_printf("!! WARNUNG - SD-KARTEN-KLASSE !!\n"); gfx_printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n"); set_color(COLOR_WHITE); gfx_printf("Diese SD-Karte erfuellt NICHT die empfohlenen\n"); gfx_printf("Mindestanforderungen (U2 | V30 | A2).\n\n"); set_color(COLOR_YELLOW); gfx_printf("Aktuelle Werte: UHS%d | V%d | A%d\n\n", (u32)sd_storage.ssr.uhs_grade, (u32)sd_storage.ssr.video_class, (u32)sd_storage.ssr.app_class); set_color(COLOR_WHITE); gfx_printf("Moegliche Folgen bei schwachen Karten:\n"); gfx_printf(" - Langsamere emuMMC / Spiel-Ladezeiten\n"); gfx_printf(" - Hoheres Risiko von Korruption oder Abstuerzen\n"); gfx_printf(" - Instabilitaet beim Schreiben groesserer Daten\n\n"); set_color(COLOR_CYAN); gfx_printf("Empfohlen: U2 | V30 | A2 oder besser.\n"); gfx_printf("Empfohlene Karten: Samsung EVO Plus, EVO Select und PRO Plus.\n\n"); set_color(COLOR_GREEN); gfx_printf("Druecke A (oder Power), um trotzdem fortzufahren.\n"); set_color(COLOR_CYAN); gfx_printf("Vol+ und Vol- (Konsole) gleichzeitig: Abbrechen -> Hekate.\n"); set_color(COLOR_WHITE); while (btn_read() & BTN_POWER) { msleep(50); } bool acknowledged = false; bool uhs_aborted = false; while (!acknowledged && !uhs_aborted) { u8 btn_state = btn_read(); if (btn_state & BTN_POWER) acknowledged = true; jc_gamepad_rpt_t *jc = joycon_poll(); if (jc) { if (jc->cap) take_screenshot(); if (jc->a) acknowledged = true; } if (cancel_combo_pressed(btn_state)) { uhs_aborted = true; break; } msleep(50); } if (uhs_aborted) { gfx_printf("\nAbgebrochen. Starte Hekate...\n"); msleep(500); if (file_exists(PAYLOAD_PATH)) { launch_payload(PAYLOAD_PATH); } else { power_set_state(POWER_OFF_REBOOT); } return; } // Wait for A and Power to be released so the same press doesn't start the install while (btn_read() & BTN_POWER) { msleep(50); } bool released = false; while (!released) { jc_gamepad_rpt_t *jc = joycon_poll(); released = (!jc || !jc->a) && !(btn_read() & BTN_POWER); if (!released) msleep(50); } msleep(300); // Short delay so the next screen is visible before accepting input gfx_clear_grey(0x1B); gfx_con_setpos(0, 0); print_header(); } // Show information set_color(COLOR_CYAN); gfx_printf("Installationsmodus: %s\n", mode == INSTALL_MODE_UPDATE ? "Update" : "Saubere Installation"); gfx_printf("Paket-Variante: %s\n", get_variant_name(pack_variant)); if (current.is_installed) { gfx_printf("Aktuelle Installation: %s\n", get_variant_name(current.variant)); } else { gfx_printf("Aktuelle Installation: Keine\n"); } set_color(COLOR_WHITE); gfx_printf("\n"); // Initialize joycons for button input jc_init_hw(); // Intro section - wait for user confirmation set_color(COLOR_YELLOW); gfx_printf("Bereit zum Installieren/Aktualisieren.\n"); set_color(COLOR_WHITE); gfx_printf("\n"); set_color(COLOR_GREEN); gfx_printf("Druecke A-Taste (rechter Joy-Con) oder Power-Taste,\n"); gfx_printf("um die Installation zu starten...\n"); set_color(COLOR_WHITE); set_color(COLOR_CYAN); gfx_printf("Vol+ und Vol- (Konsole) gleichzeitig: Abbrechen -> Hekate.\n"); set_color(COLOR_WHITE); // Wait for A/Power to start, or +/- to cancel bool button_pressed = false; bool cancelled = false; // First, wait for power button to be released if it's currently pressed while (btn_read() & BTN_POWER) { msleep(50); } while (!button_pressed && !cancelled) { // Check power button u8 btn_state = btn_read(); if (btn_state & BTN_POWER) { button_pressed = true; break; } // Check joycon buttons; Capture = screenshot jc_gamepad_rpt_t *jc = joycon_poll(); if (jc) { if (jc->cap) take_screenshot(); if (jc->a) { button_pressed = true; break; } } if (cancel_combo_pressed(btn_state)) { cancelled = true; break; } msleep(50); // Small delay to avoid busy-waiting } if (cancelled) { gfx_printf("\n"); set_color(COLOR_YELLOW); gfx_printf("Abgebrochen. Starte Hekate...\n"); set_color(COLOR_WHITE); msleep(500); if (file_exists(PAYLOAD_PATH)) { launch_payload(PAYLOAD_PATH); } else { power_set_state(POWER_OFF_REBOOT); } return; } // Clear the prompt and start installation gfx_clear_grey(0x1B); gfx_con_setpos(0, 0); print_header(); // Perform the installation set_color(COLOR_YELLOW); gfx_printf("Installation wird gestartet...\n"); set_color(COLOR_WHITE); set_color(COLOR_ORANGE); gfx_printf("Hinweis: Manchmal kann es haengen. Einfach warten.\n\n"); set_color(COLOR_WHITE); int result = perform_installation(pack_variant, mode); // Wait 3 seconds before clearing screen to allow errors to be visible msleep(3000); // take_screenshot(); // Take a screenshot of the installation summary // Clear screen for final summary to ensure it's visible gfx_clear_grey(0x1B); gfx_con_setpos(0, 0); // Summary set_color(COLOR_CYAN); gfx_printf("========================================\n"); gfx_printf(" OmniNX Installer Payload v%s\n", VERSION); gfx_printf("========================================\n\n"); set_color(COLOR_WHITE); if (result == FR_OK && total_errors == 0) { set_color(COLOR_GREEN); gfx_printf("========================================\n"); gfx_printf(" Installation abgeschlossen!\n"); gfx_printf("========================================\n"); } else { set_color(COLOR_RED); gfx_printf("========================================\n"); gfx_printf(" Installation beendet\n"); if (total_errors > 0) { gfx_printf(" %d Fehler\n", total_errors); } gfx_printf("========================================\n"); } set_color(COLOR_WHITE); // Wait for user input before launching payload gfx_printf("\n"); set_color(COLOR_GREEN); gfx_printf("Druecke A-Taste (rechter Joy-Con) oder Power-Taste,\n"); gfx_printf("um Hekate zu starten...\n"); set_color(COLOR_WHITE); // Wait for either A button or Power button (reuse button_pressed variable) button_pressed = false; // First, wait for power button to be released if it's currently pressed while (btn_read() & BTN_POWER) { msleep(50); } while (!button_pressed) { // Check power button - detect press (transition from not pressed to pressed) u8 btn_state = btn_read(); if (btn_state & BTN_POWER) { // Power button is pressed button_pressed = true; break; } // Check joycon A button; Capture = screenshot jc_gamepad_rpt_t *jc = joycon_poll(); if (jc) { if (jc->cap) take_screenshot(); if (jc->a) { button_pressed = true; break; } } msleep(50); // Small delay to avoid busy-waiting } gfx_printf("\n"); set_color(COLOR_CYAN); gfx_printf("Payload wird gestartet...\n"); set_color(COLOR_WHITE); msleep(500); // Brief delay before launch if (file_exists(PAYLOAD_PATH)) { launch_payload(PAYLOAD_PATH); } else { // Payload not found, show error set_color(COLOR_RED); gfx_printf("\nFEHLER: Payload nicht gefunden!\n"); gfx_printf("Pfad: %s\n", PAYLOAD_PATH); set_color(COLOR_WHITE); gfx_printf("\nDruecke A oder Power zum Neustart...\n"); // Wait for button again button_pressed = false; // Wait for power button to be released if pressed while (btn_read() & BTN_POWER) { msleep(50); } while (!button_pressed) { u8 btn_state = btn_read(); if (btn_state & BTN_POWER) { button_pressed = true; break; } jc_gamepad_rpt_t *jc = joycon_poll(); if (jc) { if (jc->cap) take_screenshot(); if (jc->a) { button_pressed = true; break; } } msleep(50); } power_set_state(POWER_OFF_REBOOT); } // Should never reach here while (1) bpmp_halt(); }