diff --git a/apps/keymaps/keymap-ctru.c b/apps/keymaps/keymap-ctru.c new file mode 100644 index 0000000000..0461d03192 --- /dev/null +++ b/apps/keymaps/keymap-ctru.c @@ -0,0 +1,302 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id: keymap-ctru.c 28704 2025-07-09 11:28:53Z gama $ + * + * Copyright (C) 2025 Mauricio Garrido + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +/* Button Code Definitions for Civic Type R (ctru) target */ + +#include +#include +#include + +#include "config.h" +#include "action.h" +#include "button.h" +#include "settings.h" + +/* + * The format of the list is as follows + * { Action Code, Button code, Prereq button code } + * if there's no need to check the previous button's value, use BUTTON_NONE + * Insert LAST_ITEM_IN_LIST at the end of each mapping + */ + +static const struct button_mapping button_context_standard[] = { + { ACTION_STD_PREV, BUTTON_UP, BUTTON_NONE }, + { ACTION_STD_PREVREPEAT, BUTTON_UP|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_NEXT, BUTTON_DOWN, BUTTON_NONE }, + { ACTION_STD_NEXTREPEAT, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, + + { ACTION_STD_CANCEL, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_STD_CANCEL, BUTTON_BACK|BUTTON_REL, BUTTON_BACK }, + + { ACTION_STD_OK, BUTTON_SELECT|BUTTON_REL, BUTTON_SELECT }, + { ACTION_STD_OK, BUTTON_RIGHT, BUTTON_NONE }, + + { ACTION_STD_QUICKSCREEN, BUTTON_MENU|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_CONTEXT, BUTTON_SELECT|BUTTON_REPEAT, BUTTON_SELECT }, + { ACTION_STD_MENU, BUTTON_MENU|BUTTON_REL, BUTTON_MENU }, + { ACTION_STD_CONTEXT, BUTTON_MENU|BUTTON_REL, BUTTON_NONE }, + + { ACTION_STD_KEYLOCK, BUTTON_USER|BUTTON_POWER, BUTTON_NONE }, + + LAST_ITEM_IN_LIST +}; /* button_context_standard */ + + +static const struct button_mapping button_context_mainmenu[] = { + { ACTION_TREE_WPS, BUTTON_MENU|BUTTON_REL, BUTTON_MENU }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_TREE), +}; /* button_context_mainmenu */ + +static const struct button_mapping button_context_wps[] = { + { ACTION_WPS_PLAY, BUTTON_SELECT|BUTTON_REL, BUTTON_SELECT }, + { ACTION_WPS_STOP, BUTTON_POWER|BUTTON_REPEAT, BUTTON_NONE }, + + { ACTION_WPS_BROWSE, BUTTON_BACK|BUTTON_REL, BUTTON_BACK }, + { ACTION_WPS_MENU, BUTTON_MENU|BUTTON_REL, BUTTON_MENU }, + + + { ACTION_WPS_HOTKEY, BUTTON_USER|BUTTON_REL, BUTTON_USER }, + { ACTION_WPS_PITCHSCREEN, BUTTON_USER|BUTTON_REPEAT, BUTTON_NONE }, + + { ACTION_STD_KEYLOCK, BUTTON_USER|BUTTON_POWER, BUTTON_NONE }, + + + { ACTION_WPS_CONTEXT, BUTTON_SELECT|BUTTON_REPEAT, BUTTON_SELECT }, + { ACTION_WPS_QUICKSCREEN, BUTTON_MENU|BUTTON_REPEAT, BUTTON_NONE }, + + { ACTION_WPS_SKIPPREV, BUTTON_LEFT|BUTTON_REL, BUTTON_LEFT }, + { ACTION_WPS_SEEKBACK, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_WPS_STOPSEEK, BUTTON_LEFT|BUTTON_REL, BUTTON_LEFT|BUTTON_REPEAT }, + + { ACTION_WPS_SKIPNEXT, BUTTON_RIGHT|BUTTON_REL, BUTTON_RIGHT }, + { ACTION_WPS_SEEKFWD, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_WPS_STOPSEEK, BUTTON_RIGHT|BUTTON_REL, BUTTON_RIGHT|BUTTON_REPEAT }, + + { ACTION_WPS_VOLUP, BUTTON_UP|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_WPS_VOLUP, BUTTON_UP, BUTTON_NONE }, + { ACTION_WPS_VOLDOWN, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_WPS_VOLDOWN, BUTTON_DOWN, BUTTON_NONE }, + + LAST_ITEM_IN_LIST +}; /* button_context_wps */ + +static const struct button_mapping button_context_list[] = { + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; /* button_context_list */ + +static const struct button_mapping button_context_tree[] = { + { ACTION_TREE_WPS, BUTTON_USER|BUTTON_REPEAT, BUTTON_USER }, + { ACTION_TREE_STOP, BUTTON_POWER|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_TREE_HOTKEY, BUTTON_USER|BUTTON_REL, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_LIST) +}; /* button_context_tree */ + +static const struct button_mapping button_context_settings[] = { + { ACTION_SETTINGS_INC, BUTTON_UP, BUTTON_NONE }, + { ACTION_SETTINGS_INCREPEAT, BUTTON_UP|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_SETTINGS_DEC, BUTTON_DOWN, BUTTON_NONE }, + { ACTION_SETTINGS_DECREPEAT, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_PREV, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_STD_PREVREPEAT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_NEXT, BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_STD_NEXTREPEAT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_TREE) +}; /* button_context_settings */ + +static const struct button_mapping button_context_settings_right_is_inc[] = { + { ACTION_SETTINGS_INC, BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_SETTINGS_INCREPEAT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_SETTINGS_DEC, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_SETTINGS_DECREPEAT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; /* button_context_settings_right_is_inc */ + +static const struct button_mapping button_context_yesno[] = { + { ACTION_YESNO_ACCEPT, BUTTON_SELECT, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; /* button_context_settings_yesno */ + +static const struct button_mapping button_context_colorchooser[] = { //check + { ACTION_STD_OK, BUTTON_SELECT|BUTTON_REL, BUTTON_SELECT }, + { ACTION_STD_CANCEL, BUTTON_BACK, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_CUSTOM|CONTEXT_SETTINGS), +}; /* button_context_colorchooser */ + +static const struct button_mapping button_context_eq[] = { + { ACTION_STD_CANCEL, BUTTON_MENU|BUTTON_REL, BUTTON_MENU }, + { ACTION_SETTINGS_INC, BUTTON_RIGHT , BUTTON_NONE }, + { ACTION_SETTINGS_INCREPEAT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_SETTINGS_DEC, BUTTON_LEFT , BUTTON_NONE }, + { ACTION_SETTINGS_DECREPEAT, BUTTON_LEFT|BUTTON_REPEAT , BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_CUSTOM|CONTEXT_SETTINGS), +}; /* button_context_eq */ + +/** Bookmark Screen **/ +static const struct button_mapping button_context_bmark[] = { + { ACTION_BMS_DELETE, BUTTON_POWER, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_LIST), +}; /* button_context_bmark */ + +static const struct button_mapping button_context_time[] = { + { ACTION_SETTINGS_INC, BUTTON_UP, BUTTON_NONE }, + { ACTION_SETTINGS_INCREPEAT, BUTTON_UP|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_SETTINGS_DEC, BUTTON_DOWN, BUTTON_NONE }, + { ACTION_SETTINGS_DECREPEAT, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_PREV, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_STD_PREVREPEAT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_NEXT, BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_STD_NEXTREPEAT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD), +}; /* button_context_time */ + +static const struct button_mapping button_context_quickscreen[] = { + { ACTION_QS_TOP, BUTTON_UP, BUTTON_NONE }, + { ACTION_QS_TOP, BUTTON_UP|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_QS_DOWN, BUTTON_DOWN, BUTTON_NONE }, + { ACTION_QS_DOWN, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_QS_LEFT, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_QS_LEFT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_QS_RIGHT, BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_QS_RIGHT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_CANCEL, BUTTON_MENU, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; /* button_context_quickscreen */ + +static const struct button_mapping button_context_pitchscreen[] = { + { ACTION_PS_INC_SMALL, BUTTON_UP, BUTTON_NONE }, + { ACTION_PS_INC_BIG, BUTTON_UP|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_PS_DEC_SMALL, BUTTON_DOWN, BUTTON_NONE }, + { ACTION_PS_DEC_BIG, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, + + { ACTION_PS_SLOWER, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_PS_FASTER, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + + { ACTION_PS_NUDGE_LEFT, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_PS_NUDGE_LEFTOFF, BUTTON_LEFT|BUTTON_REL, BUTTON_NONE }, + { ACTION_PS_NUDGE_RIGHT, BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_PS_NUDGE_RIGHTOFF, BUTTON_RIGHT|BUTTON_REL, BUTTON_NONE }, + + { ACTION_PS_RESET, BUTTON_SELECT, BUTTON_NONE }, + { ACTION_PS_TOGGLE_MODE, BUTTON_USER, BUTTON_NONE }, + { ACTION_PS_EXIT, BUTTON_MENU|BUTTON_REL, BUTTON_NONE }, + { ACTION_PS_EXIT, BUTTON_BACK|BUTTON_REL, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; /* button_context_pitchcreen */ + +static const struct button_mapping button_context_keyboard[] = { + { ACTION_KBD_UP, BUTTON_UP, BUTTON_NONE }, + { ACTION_KBD_UP, BUTTON_UP|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_KBD_DOWN, BUTTON_DOWN, BUTTON_NONE }, + { ACTION_KBD_DOWN, BUTTON_DOWN|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_KBD_LEFT, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_KBD_LEFT, BUTTON_LEFT|BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_KBD_RIGHT, BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_KBD_RIGHT, BUTTON_RIGHT|BUTTON_REPEAT, BUTTON_NONE }, + + { ACTION_KBD_SELECT, BUTTON_SELECT, BUTTON_NONE }, + { ACTION_KBD_ABORT, BUTTON_BACK|BUTTON_REL, BUTTON_BACK }, + { ACTION_KBD_DONE, BUTTON_MENU|BUTTON_REL, BUTTON_MENU }, + { ACTION_KBD_BACKSPACE, BUTTON_USER, BUTTON_NONE }, + { ACTION_KBD_PAGE_FLIP, BUTTON_POWER, BUTTON_NONE }, + + LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD) +}; /* button_context_keyboard */ + +static const struct button_mapping button_context_radio[] = { + { ACTION_FM_MENU, BUTTON_SELECT | BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_FM_PRESET, BUTTON_MENU | BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_FM_STOP, BUTTON_POWER | BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_FM_MODE, BUTTON_MENU | BUTTON_REL, BUTTON_MENU }, + { ACTION_FM_EXIT, BUTTON_BACK | BUTTON_REL, BUTTON_BACK }, + { ACTION_FM_PLAY, BUTTON_SELECT | BUTTON_REL, BUTTON_SELECT }, + { ACTION_FM_NEXT_PRESET, BUTTON_USER | BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_FM_PREV_PRESET, BUTTON_USER | BUTTON_LEFT, BUTTON_NONE }, + + /* Volume */ + { ACTION_SETTINGS_INC, BUTTON_UP | BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_SETTINGS_INCREPEAT, BUTTON_UP, BUTTON_NONE }, + { ACTION_SETTINGS_DEC, BUTTON_DOWN | BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_SETTINGS_DECREPEAT, BUTTON_DOWN, BUTTON_NONE }, + /* Tuning */ + { ACTION_STD_PREV, BUTTON_LEFT, BUTTON_NONE }, + { ACTION_STD_PREVREPEAT, BUTTON_LEFT | BUTTON_REPEAT, BUTTON_NONE }, + { ACTION_STD_NEXT, BUTTON_RIGHT, BUTTON_NONE }, + { ACTION_STD_NEXTREPEAT, BUTTON_RIGHT | BUTTON_REPEAT, BUTTON_NONE }, +}; /* button_context_radio */ + +const struct button_mapping* target_get_context_mapping(int context) +{ + switch (context & ~CONTEXT_LOCKED) + { + case CONTEXT_STD: + return button_context_standard; + case CONTEXT_WPS: + return button_context_wps; + + case CONTEXT_LIST: + return button_context_list; + case CONTEXT_MAINMENU: + return button_context_mainmenu; + + case CONTEXT_TREE: + return button_context_tree; + + case CONTEXT_SETTINGS: + return button_context_settings; + + case CONTEXT_CUSTOM|CONTEXT_SETTINGS: + case CONTEXT_SETTINGS_RECTRIGGER: + return button_context_settings_right_is_inc; + + case CONTEXT_SETTINGS_COLOURCHOOSER: + return button_context_colorchooser; + case CONTEXT_SETTINGS_EQ: + return button_context_eq; + + case CONTEXT_SETTINGS_TIME: + return button_context_time; + + case CONTEXT_YESNOSCREEN: + return button_context_yesno; + case CONTEXT_FM: + return button_context_radio; + case CONTEXT_BOOKMARKSCREEN: + return button_context_bmark; + case CONTEXT_QUICKSCREEN: + return button_context_quickscreen; + case CONTEXT_PITCHSCREEN: + return button_context_pitchscreen; + case CONTEXT_KEYBOARD: + return button_context_keyboard; + } + return button_context_standard; +} diff --git a/firmware/drivers/audio/ctru.c b/firmware/drivers/audio/ctru.c new file mode 100644 index 0000000000..70a4170557 --- /dev/null +++ b/firmware/drivers/audio/ctru.c @@ -0,0 +1,92 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright © 2010 Thomas Martitz + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "config.h" +#include "sound.h" +#include "pcm_sampr.h" +#ifdef HAVE_SW_VOLUME_CONTROL +#include "pcm_sw_volume.h" +#include "fixedpoint.h" +#endif + +/** + * Audio Hardware api. Make some of them do nothing as we cannot properly + * simulate with SDL. if we used DSP we would run code that doesn't actually + * run on the target + **/ + +void audiohw_set_volume(int vol_l, int vol_r) +{ + (void)vol_l; (void)vol_r; +} + +#if defined(AUDIOHW_HAVE_BALANCE) +void audiohw_set_balance(int value) { (void)value; } +#endif +#ifndef HAVE_SW_TONE_CONTROLS +#if defined(AUDIOHW_HAVE_BASS) +void audiohw_set_bass(int value) { (void)value; } +#endif +#if defined(AUDIOHW_HAVE_TREBLE) +void audiohw_set_treble(int value) { (void)value; } +#endif +#endif /* HAVE_SW_TONE_CONTROLS */ +#if defined(AUDIOHW_HAVE_BASS_CUTOFF) +void audiohw_set_bass_cutoff(int value) { (void)value; } +#endif +#if defined(AUDIOHW_HAVE_TREBLE_CUTOFF) +void audiohw_set_treble_cutoff(int value){ (void)value; } +#endif +/* EQ-based tone controls */ +#if defined(AUDIOHW_HAVE_EQ) +void audiohw_set_eq_band_gain(unsigned int band, int value) + { (void)band; (void)value; } +#endif +#if defined(AUDIOHW_HAVE_EQ_FREQUENCY) +void audiohw_set_eq_band_frequency(unsigned int band, int value) + { (void)band; (void)value; } +#endif +#if defined(AUDIOHW_HAVE_EQ_WIDTH) +void audiohw_set_eq_band_width(unsigned int band, int value) + { (void)band; (void)value; } +#endif +#if defined(AUDIOHW_HAVE_DEPTH_3D) +void audiohw_set_depth_3d(int value) + { (void)value; } +#endif +#if defined(AUDIOHW_HAVE_LINEOUT) +void audiohw_set_lineout_volume(int vol_l, int vol_r) + { (void)vol_l; (void)vol_r; } +#endif +#if defined(AUDIOHW_HAVE_FILTER_ROLL_OFF) +void audiohw_set_filter_roll_off(int value) + { (void)value; } +#endif +#if defined(AUDIOHW_HAVE_POWER_MODE) +void audiohw_set_power_mode(int value) + { (void)value; } +#endif + +#ifdef CONFIG_SAMPR_TYPES +unsigned int pcm_sampr_to_hw_sampr(unsigned int samplerate, + unsigned int type) + { return samplerate; (void)type; } +#endif /* CONFIG_SAMPR_TYPES */ diff --git a/firmware/export/config/ctru.h b/firmware/export/config/ctru.h new file mode 100644 index 0000000000..0edaf145a5 --- /dev/null +++ b/firmware/export/config/ctru.h @@ -0,0 +1,102 @@ +/* + * This config file is for the N3DS hosted application + */ + +/* We don't run on hardware directly */ +#define CONFIG_PLATFORM (PLATFORM_HOSTED|PLATFORM_CTRU) +#define HAVE_FPU + +/* For Rolo and boot loader */ +#define MODEL_NUMBER 100s +#define MODEL_NAME "CTRU" + +#define USB_NONE + +#define CONFIG_CPU N10480H + +#define CPU_FREQ 268000000 + +/* Define this if you have adjustable CPU frequency */ +/* #define HAVE_ADJUSTABLE_CPU_FREQ */ + +/* define this if you have a colour LCD */ +#define HAVE_LCD_COLOR + +/* define this if you want album art for this target */ +#define HAVE_ALBUMART + +/* define this to enable bitmap scaling */ +#define HAVE_BMP_SCALING + +/* define this to enable JPEG decoding */ +#define HAVE_JPEG + +/* define this if you have access to the quickscreen */ +#define HAVE_QUICKSCREEN + +/* define this if you would like tagcache to build on this target */ +#define HAVE_TAGCACHE + +/* LCD dimensions */ +#define LCD_WIDTH 320 +#define LCD_HEIGHT 240 + +#define LCD_DEPTH 16 +#define LCD_PIXELFORMAT RGB565 + +#define LCD_OPTIMIZED_UPDATE +#define LCD_OPTIMIZED_UPDATE_RECT +#define LCD_OPTIMIZED_BLIT_YUV + +/* define this to indicate your device's keypad */ +#define HAVE_TOUCHSCREEN +#define HAVE_BUTTON_DATA + +/* define this if you have a real-time clock */ +#define CONFIG_RTC APPLICATION + +/* Power management */ +#define CONFIG_BATTERY_MEASURE PERCENTAGE_MEASURE +#define CONFIG_CHARGING CHARGING_MONITOR +#define HAVE_SW_POWEROFF + +/* The number of bytes reserved for loadable codecs */ +#define CODEC_SIZE 0x100000 + +/* The number of bytes reserved for loadable plugins */ +#define PLUGIN_BUFFER_SIZE 0x80000 + +#define AB_REPEAT_ENABLE + + +/* #define HAVE_SCROLLWHEEL */ +#define CONFIG_KEYPAD CTRU_PAD + +#define HAVE_CTRU_AUDIO +#define HAVE_HEADPHONE_DETECTION +/* #define HAVE_SW_VOLUME_CONTROL */ +/* #define PCM_SW_VOLUME_UNBUFFERED */ +/* #define PCM_SW_VOLUME_FRACBITS (16) */ + +/* Define this for LCD backlight available */ +#define HAVE_BACKLIGHT +#define HAVE_BACKLIGHT_BRIGHTNESS + +/* Main LCD backlight brightness range and defaults */ +#define MIN_BRIGHTNESS_SETTING 16 +#define MAX_BRIGHTNESS_SETTING 142 +#define BRIGHTNESS_STEP 5 +#define DEFAULT_BRIGHTNESS_SETTING 28 +#define CONFIG_BACKLIGHT_FADING BACKLIGHT_FADING_SW_SETTING + +#define CONFIG_LCD LCD_COWOND2 + +/* Define this if a programmable hotkey is mapped */ +#define HAVE_HOTKEY + +#define BOOTDIR "/" + +/* No special storage */ +#define CONFIG_STORAGE STORAGE_HOSTFS +#define HAVE_STORAGE_FLUSH + diff --git a/firmware/target/hosted/ctru/app/adc-target.h b/firmware/target/hosted/ctru/app/adc-target.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/firmware/target/hosted/ctru/app/backlight-target.h b/firmware/target/hosted/ctru/app/backlight-target.h new file mode 100644 index 0000000000..b29005f7e1 --- /dev/null +++ b/firmware/target/hosted/ctru/app/backlight-target.h @@ -0,0 +1,32 @@ +/*************************************************************************** + * __________ __ ___ + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2017 Marcin Bukat + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef _BACKLIGHT_TARGET_H_ +#define _BACKLIGHT_TARGET_H_ + +#include + +/* See backlight.c */ +bool backlight_hw_init(void); +void backlight_hw_on(void); +void backlight_hw_off(void); +void backlight_hw_brightness(int brightness); + +#endif diff --git a/firmware/target/hosted/ctru/app/button-target.h b/firmware/target/hosted/ctru/app/button-target.h new file mode 100644 index 0000000000..36fed8b59d --- /dev/null +++ b/firmware/target/hosted/ctru/app/button-target.h @@ -0,0 +1,54 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2007 by Rob Purchase + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef _BUTTON_TARGET_H_ +#define _BUTTON_TARGET_H_ + +/* Logical buttons key codes */ +#define BUTTON_UP 0x00000001 +#define BUTTON_DOWN 0x00000002 +#define BUTTON_LEFT 0x00000004 +#define BUTTON_RIGHT 0x00000008 +#define BUTTON_USER 0x00000010 +#define BUTTON_MENU 0x00000020 +#define BUTTON_BACK 0x00000040 +#define BUTTON_POWER 0x00000080 +#define BUTTON_SELECT 0x00000100 + +/* Touch Screen Area Buttons */ +#define BUTTON_TOPLEFT 0x00001000 +#define BUTTON_TOPMIDDLE 0x00002000 +#define BUTTON_TOPRIGHT 0x00004000 +#define BUTTON_MIDLEFT 0x00008000 +#define BUTTON_CENTER 0x00010000 +#define BUTTON_MIDRIGHT 0x00020000 +#define BUTTON_BOTTOMLEFT 0x00040000 +#define BUTTON_BOTTOMMIDDLE 0x00080000 +#define BUTTON_BOTTOMRIGHT 0x00100000 + +#define BUTTON_MAIN 0x1FFF + +/* Software power-off */ +#define POWEROFF_BUTTON BUTTON_POWER +/* About 3 seconds */ +#define POWEROFF_COUNT 10 + +#endif /* _BUTTON_TARGET_H_ */ diff --git a/firmware/target/hosted/ctru/backlight-ctru.c b/firmware/target/hosted/ctru/backlight-ctru.c new file mode 100644 index 0000000000..917db29405 --- /dev/null +++ b/firmware/target/hosted/ctru/backlight-ctru.c @@ -0,0 +1,114 @@ +/*************************************************************************** + * __________ __ ___ + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "backlight-target.h" +#include "sysfs.h" +#include "panic.h" +#include "lcd.h" +#include "debug.h" + +#include <3ds/services/gsplcd.h> +#include <3ds/result.h> +#include "luminance-ctru.h" + +/* TODO: To use calibrated values in rockbox, + MIN_BRIGHTNESS_SETTING (etc) would need to be + declared as constants in settings-list.c */ + +u32 ctru_min_lum = 16; +u32 ctru_max_lum = 142; +u32 ctru_luminance = 28; + +/* FIXME: After calling gspLcdInit() the home menu will no longer be + accesible, to fix this we have to call gspLcdInit/gspLcdExit as + a mutex lock/unlock when using gsplcd.h functions. */ +void lcd_mutex_lock(void) +{ + Result res = gspLcdInit(); + if (R_FAILED(res)) { + DEBUGF("backlight_hw_init: gspLcdInit failed.\n"); + } +} + +void lcd_mutex_unlock(void) +{ + gspLcdExit(); +} + +bool backlight_hw_init(void) +{ + /* read calibrated values */ + ctru_luminance = getCurrentLuminance(false); + ctru_min_lum = getMinLuminancePreset(); + ctru_max_lum = getMaxLuminancePreset(); + + backlight_hw_on(); + backlight_hw_brightness(DEFAULT_BRIGHTNESS_SETTING); + return true; +} + +static int last_bl = -1; + +void backlight_hw_on(void) +{ + lcd_mutex_lock(); + if (last_bl != 1) { +#ifdef HAVE_LCD_ENABLE + lcd_enable(true); +#endif + GSPLCD_PowerOnAllBacklights(); + last_bl = 1; + } + lcd_mutex_unlock(); +} + +void backlight_hw_off(void) +{ + lcd_mutex_lock(); + if (last_bl != 0) { + /* only power off rockbox ui screen */ + GSPLCD_PowerOffBacklight(GSPLCD_SCREEN_BOTTOM); +#ifdef HAVE_LCD_ENABLE + lcd_enable(false); +#endif + last_bl = 0; + } + lcd_mutex_unlock(); +} + +void backlight_hw_brightness(int brightness) +{ + /* cap range, just in case */ + if (brightness > MAX_BRIGHTNESS_SETTING) + brightness = MAX_BRIGHTNESS_SETTING; + if (brightness < MIN_BRIGHTNESS_SETTING) + brightness = MIN_BRIGHTNESS_SETTING; + + /* normalize level on both screens */ + lcd_mutex_lock(); + GSPLCD_SetBrightnessRaw(GSPLCD_SCREEN_TOP | GSPLCD_SCREEN_BOTTOM, (u32) brightness); + lcd_mutex_unlock(); +} diff --git a/firmware/target/hosted/ctru/button-ctru.c b/firmware/target/hosted/ctru/button-ctru.c new file mode 100644 index 0000000000..c28d7e56ae --- /dev/null +++ b/firmware/target/hosted/ctru/button-ctru.c @@ -0,0 +1,184 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2002 by Felix Arends + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include /* EXIT_SUCCESS */ +#include +#include "config.h" +#include "button.h" +#include "kernel.h" +#include "backlight.h" +#include "system.h" +#include "button-ctru.h" +#include "buttonmap.h" +#include "debug.h" +#include "powermgmt.h" +#include "storage.h" +#include "settings.h" +#include "sound.h" +#include "misc.h" + +#include "touchscreen.h" + +#include <3ds/types.h> +#include <3ds/services/apt.h> +#include <3ds/services/hid.h> +#include <3ds/services/mcuhwc.h> +#include <3ds/services/dsp.h> + +static u8 old_slider_level = 0; +static int last_y, last_x; + +static enum { + STATE_UNKNOWN = -1, + STATE_UP = 0, + STATE_DOWN = 1, +} last_touch_state = STATE_UNKNOWN; + +static double map_values(double n, double source_start, double source_end, double dest_start, double dest_end, int decimal_precision ) { + double delta_start = source_end - source_start; + double delta_end = dest_end - dest_start; + if(delta_start == 0.0 || delta_end == 0.0) { + return 1.0; + } + double scale = delta_end / delta_start; + double neg_start = -1.0 * source_start; + double offset = (neg_start * scale) + dest_start; + double final_number = (n * scale) + offset; + int calc_scale = (int) pow(10.0, decimal_precision); + return (double) round(final_number * calc_scale) / calc_scale; +} + +void update_sound_slider_level(void) +{ + /* update global volume based on sound slider level */ + u8 level; + MCUHWC_GetSoundSliderLevel(&level); + + if (level != old_slider_level) { + int volume = (int) map_values((double) level, + 0.0, /* min slider voslume */ + (double) 0x3f, /* max slider value */ + (double) sound_min(SOUND_VOLUME), + (double) sound_max(SOUND_VOLUME), + 0); + global_status.volume = volume; + setvol(); + old_slider_level = level; + } +} + +int button_read_device(int* data) +{ + int key = BUTTON_NONE; + + /* TODO: implement Home Menu button support */ + /* if (!aptMainLoop()) { + return true; + } */ + + hidScanInput(); + u32 kDown = hidKeysDown(); + + if (kDown & KEY_SELECT) { + touchscreen_set_mode(touchscreen_get_mode() == TOUCHSCREEN_POINT ? TOUCHSCREEN_BUTTON : TOUCHSCREEN_POINT); + printf("Touchscreen mode: %s\n", touchscreen_get_mode() == TOUCHSCREEN_POINT ? "TOUCHSCREEN_POINT" : "TOUCHSCREEN_BUTTON"); + } + + u32 kHeld = hidKeysHeld(); + + /* rockbox will handle button repeats */ + kDown |= kHeld; + + /* Check for all the keys */ + if (kDown & KEY_A) { + key |= BUTTON_SELECT; + } + if (kDown & KEY_B) { + key |= BUTTON_BACK; + } + if (kDown & KEY_X) { + key |= BUTTON_MENU; + } + if (kDown & KEY_Y) { + key |= BUTTON_USER; + } + if (kDown & KEY_START) { + key |= BUTTON_POWER; + } + if (kDown & KEY_DRIGHT) { + key |= BUTTON_RIGHT; + } + if (kDown & KEY_DLEFT) { + key |= BUTTON_LEFT; + } + if (kDown & KEY_DUP) { + key |= BUTTON_UP; + } + if (kDown & KEY_DDOWN) { + key |= BUTTON_DOWN; + } + if (kDown & KEY_START) { + key |= BUTTON_POWER; + } + + touchPosition touch; + hidTouchRead(&touch); + + /* Generate UP and DOWN events */ + if (kDown & KEY_TOUCH) { + last_touch_state = STATE_DOWN; + } + else { + last_touch_state = STATE_UP; + } + + last_x = touch.px; + last_y = touch.py; + + int tkey = touchscreen_to_pixels(last_x, last_y, data); + if (last_touch_state == STATE_DOWN) { + key |= tkey; + } + + update_sound_slider_level(); + + return key; +} + +void button_init_device(void) +{ + hidInit(); +} + +#ifndef HAS_BUTTON_HOLD +void touchscreen_enable_device(bool en) +{ + (void)en; +} +#endif + +bool headphones_inserted(void) +{ + bool is_inserted; + DSP_GetHeadphoneStatus(&is_inserted); + return is_inserted; +} diff --git a/firmware/target/hosted/ctru/button-ctru.h b/firmware/target/hosted/ctru/button-ctru.h new file mode 100644 index 0000000000..637fcbf077 --- /dev/null +++ b/firmware/target/hosted/ctru/button-ctru.h @@ -0,0 +1,34 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2009 by Thomas Martitz + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + + +#ifndef __BUTTON_CTRU_H__ +#define __BUTTON_CTRU_H__ + +#include +#include "config.h" + +bool button_hold(void); +#undef button_init_device +void button_init_device(void); +int button_read_device(int *data); + +#endif /* __BUTTON_CTRU_H__ */ diff --git a/firmware/target/hosted/ctru/buttonmap.h b/firmware/target/hosted/ctru/buttonmap.h new file mode 100644 index 0000000000..b6fcaa2efc --- /dev/null +++ b/firmware/target/hosted/ctru/buttonmap.h @@ -0,0 +1,41 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2010 by Fred Bauer + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __BUTTONMAP_H__ +#define __BUTTONMAP_H__ +/* Button maps: simulated key, x, y, radius, name */ +/* Run sim with --mapping to get coordinates */ +/* or --debugbuttons to check */ +/* The First matching button is returned */ +struct button_map { + int button, x, y, radius; + char *description; +}; + +extern struct button_map bm[]; + +int xy2button( int x, int y); + +#ifdef HAVE_TOUCHSCREEN +int key_to_touch(int keyboard_button, unsigned int mouse_coords); +#endif + +#endif /* __BUTTONMAP_H__ */ diff --git a/firmware/target/hosted/ctru/filesystem-ctru.c b/firmware/target/hosted/ctru/filesystem-ctru.c new file mode 100644 index 0000000000..607a2e2ed6 --- /dev/null +++ b/firmware/target/hosted/ctru/filesystem-ctru.c @@ -0,0 +1,73 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2010 by Thomas Martitz + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#define RB_FILESYSTEM_OS +#include +#include +#include +#include +#include +#include "config.h" +#include "system.h" +#include "file.h" +#include "dir.h" +#include "mv.h" +#include "debug.h" +#include "pathfuncs.h" +#include "string-extra.h" + +#include <3ds/archive.h> + +void paths_init(void) +{ + /* is this needed in 3DS? */ +#if 0 + char config_dir[MAX_PATH]; + + const char *home = "/3ds"; + mkdir("/3ds/.rockbox" __MKDIR_MODE_ARG); + + snprintf(config_dir, sizeof(config_dir), "%s/.config", home); + mkdir(config_dir __MKDIR_MODE_ARG); + snprintf(config_dir, sizeof(config_dir), "%s/.config/rockbox.org", home); + mkdir(config_dir __MKDIR_MODE_ARG); + /* Plugin data directory */ + snprintf(config_dir, sizeof(config_dir), "%s/.config/rockbox.org/rocks.data", home); + mkdir(config_dir __MKDIR_MODE_ARG); +#endif +} + +/* only sdcard volume is accesible to the user */ +void volume_size(IF_MV(int volume,) sector_t *sizep, sector_t *freep) +{ + sector_t size = 0, free = 0; + + FS_ArchiveResource sdmcRSRC; + FSUSER_GetSdmcArchiveResource(&sdmcRSRC); + + size = (sdmcRSRC.totalClusters * sdmcRSRC.clusterSize) / sdmcRSRC.sectorSize; + free = (sdmcRSRC.freeClusters * sdmcRSRC.clusterSize) / sdmcRSRC.sectorSize; + + if (sizep) + *sizep = size; + + if (freep) + *freep = free; +} diff --git a/firmware/target/hosted/ctru/filesystem-ctru.h b/firmware/target/hosted/ctru/filesystem-ctru.h new file mode 100644 index 0000000000..a67c7b58a0 --- /dev/null +++ b/firmware/target/hosted/ctru/filesystem-ctru.h @@ -0,0 +1,116 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 by Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef _FILESYSTEM_CTRU_H_ +#define _FILESYSTEM_CTRU_H_ + +#if defined(PLUGIN) || defined(CODEC) +#define FILEFUNCTIONS_DECLARED +#define FILEFUNCTIONS_DEFINED +#define DIRFUNCTIONS_DECLARED +#define DIRFUNCTIONS_DEFINED +#define OSFUNCTIONS_DECLARED +#endif /* PLUGIN || CODEC */ + +#ifndef OSFUNCTIONS_DECLARED +#define FS_PREFIX(_x_) ctru_ ## _x_ + +void paths_init(void); +#endif /* !OSFUNCTIONS_DECLARED */ +#endif /* _FILESYSTEM_CTRU_H_ */ + +#ifdef _FILE_H_ +#include + +#ifndef _FILESYSTEM_CTRU__FILE_H_ +#define _FILESYSTEM_CTRU__FILE_H_ + +#ifdef RB_FILESYSTEM_OS +#define FILEFUNCTIONS_DEFINED +#endif + +#ifndef FILEFUNCTIONS_DECLARED +#define __OPEN_MODE_ARG +#define __CREAT_MODE_ARG \ + , mode + +#include + +int ctru_open(const char *name, int oflag, ...); +int ctru_creat(const char *name, mode_t mode); +int ctru_close(int fildes); +int ctru_ftruncate(int fildes, off_t length); +int ctru_fsync(int fildes); +off_t ctru_lseek(int fildes, off_t offset, int whence); +ssize_t ctru_read(int fildes, void *buf, size_t nbyte); +ssize_t ctru_write(int fildes, const void *buf, size_t nbyte); +int ctru_remove(const char *path); +int ctru_rename(const char *old, const char *new); +int ctru_modtime(const char *path, time_t modtime); +off_t ctru_filesize(int fildes); +int ctru_fsamefile(int fildes1, int fildes2); +int ctru_relate(const char *path1, const char *path2); +bool ctru_file_exists(const char *path); +ssize_t ctru_readlink(const char *path, char *buf, size_t bufsiz); + +#endif /* !FILEFUNCTIONS_DECLARED */ + +#endif /* _FILESYSTEM_CTRU__FILE_H_ */ +#endif /* _FILE_H_ */ + +#ifdef _DIR_H_ +#ifndef _FILESYSTEM_CTRU__DIR_H_ +#define _FILESYSTEM_CTRU__DIR_H_ + +#define DIRENT dirent +struct dirent; + +struct dirinfo_native +{ + unsigned int attr; + off_t size; + uint16_t wrtdate; + uint16_t wrttime; +}; + +typedef struct {} DIR; + +#ifndef DIRFUNCTIONS_DECLARED +#define __MKDIR_MODE_ARG \ + , 0777 + +#ifdef RB_FILESYSTEM_OS +#define DIRFUNCTIONS_DEFINED +#endif + +DIR * ctru_opendir(const char *dirname); +struct dirent * ctru_readdir(DIR *dirp); +int ctru_readdir_r(DIR *dirp, struct dirent *entry, + struct dirent **result); +void ctru_rewinddir(DIR *dirp); +int ctru_closedir(DIR *dirp); +int ctru_mkdir(const char *path); +int ctru_rmdir(const char *path); +int ctru_samedir(DIR *dirp1, DIR *dirp2); +bool ctru_dir_exists(const char *dirname); +#endif /* !DIRFUNCTIONS_DECLARED */ + +#endif /* _FILESYSTEM_CTRU__DIR_H_ */ +#endif /* _DIR_H_ */ diff --git a/firmware/target/hosted/ctru/kernel-ctru.c b/firmware/target/hosted/ctru/kernel-ctru.c new file mode 100644 index 0000000000..e035af282d --- /dev/null +++ b/firmware/target/hosted/ctru/kernel-ctru.c @@ -0,0 +1,166 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include "system-ctru.h" +#include "thread-ctru.h" +#include "kernel.h" +#include "thread.h" +#include "panic.h" +#include "debug.h" + +static int tick_timer_id; +long start_tick; + +/* Condition to signal that "interrupts" may proceed */ +static sysCond *sim_thread_cond; +/* Mutex to serialize changing levels and exclude other threads while + * inside a handler */ +static RecursiveLock sim_irq_mtx; +/* Level: 0 = enabled, not 0 = disabled */ +static int volatile interrupt_level = HIGHEST_IRQ_LEVEL; +/* How many handers waiting? Not strictly needed because CondSignal is a + * noop if no threads were waiting but it filters-out calls to functions + * with higher overhead and provides info when debugging. */ +static int handlers_pending = 0; +/* 1 = executing a handler; prevents CondSignal calls in set_irq_level + * while in a handler */ +static int status_reg = 0; + +/* Nescessary logic: + * 1) All threads must pass unblocked + * 2) Current handler must always pass unblocked + * 3) Threads must be excluded when irq routine is running + * 4) No more than one handler routine should execute at a time + */ +int set_irq_level(int level) +{ + RecursiveLock_Lock(&sim_irq_mtx); + + int oldlevel = interrupt_level; + + if (status_reg == 0 && level == 0 && oldlevel != 0) + { + /* Not in a handler and "interrupts" are going from disabled to + * enabled; signal any pending handlers still waiting */ + if (handlers_pending > 0) + sys_cond_broadcast(sim_thread_cond); + } + + interrupt_level = level; /* save new level */ + + RecursiveLock_Unlock(&sim_irq_mtx); + return oldlevel; +} + +void sim_enter_irq_handler(void) +{ + RecursiveLock_Lock(&sim_irq_mtx); + handlers_pending++; + + /* Check each time before proceeding: disabled->enabled->...->disabled + * is possible on an app thread before a handler thread is ever granted + * the mutex; a handler can also leave "interrupts" disabled during + * its execution */ + while (interrupt_level != 0) + sys_cond_wait(sim_thread_cond, &sim_irq_mtx); + + status_reg = 1; +} + +void sim_exit_irq_handler(void) +{ + /* If any others are waiting, give the signal */ + if (--handlers_pending > 0) + sys_cond_signal(sim_thread_cond); + + status_reg = 0; + RecursiveLock_Unlock(&sim_irq_mtx); +} + +static bool sim_kernel_init(void) +{ + RecursiveLock_Init(&sim_irq_mtx); + sim_thread_cond = sys_cond_create(); + if (sim_thread_cond == NULL) + { + panicf("Cannot create sim_thread_cond\n"); + return false; + } + return true; +} + +void sim_kernel_shutdown(void) +{ + sys_remove_timer(tick_timer_id); + enable_irq(); + while(handlers_pending > 0) + sys_delay(10); + + sys_cond_destroy(sim_thread_cond); +} + +u32 tick_timer(u32 interval, void *param) +{ + long new_tick; + + (void) interval; + (void) param; + + new_tick = (sys_get_ticks() - start_tick) / (1000/HZ); + + while(new_tick != current_tick) + { + sim_enter_irq_handler(); + + /* Run through the list of tick tasks - increments tick + * on each iteration. */ + call_tick_tasks(); + + sim_exit_irq_handler(); + } + + return interval; +} + +void tick_start(unsigned int interval_in_ms) +{ + if (!sim_kernel_init()) + { + panicf("Could not initialize kernel!"); + exit(-1); + } + + if (tick_timer_id != 0) + { + sys_remove_timer(tick_timer_id); + tick_timer_id = 0; + } + else + { + start_tick = sys_get_ticks(); + } + + tick_timer_id = sys_add_timer(interval_in_ms, tick_timer, NULL); +} + diff --git a/firmware/target/hosted/ctru/lc-ctru.c b/firmware/target/hosted/ctru/lc-ctru.c new file mode 100644 index 0000000000..e65adbbf5a --- /dev/null +++ b/firmware/target/hosted/ctru/lc-ctru.c @@ -0,0 +1,68 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#define RB_FILESYSTEM_OS +#include +#include +#include +#include "system.h" +#include "load_code.h" +#include "filesystem-ctru.h" +#include "debug.h" + +void* programResolver(const char* sym, void *userData); +void * lc_open(const char *filename, unsigned char *buf, size_t buf_size) +{ + DEBUGF("dlopen(path=\"%s\")\n", filename); + + /* note: the 3ds dlopen implementation needs a custom resolver + for the unresolved symbols in shared objects */ + /* void *handle = dlopen(filename, RTLD_NOW | RTLD_LOCAL); */ + void *handle = ctrdlOpen(filename, + RTLD_NOW | RTLD_LOCAL, + programResolver, + NULL); + if (handle == NULL) + { + DEBUGF("%s(\"%s\") failed\n", __func__, filename); + DEBUGF(" lc_open error '%s'\n", dlerror()); + } + DEBUGF("handle = %p\n", handle); + + return handle; + (void) buf; (void) buf_size; +} + +void * lc_get_header(void *handle) +{ + void *symbol = dlsym(handle, "__header"); + if (!symbol) { + symbol = dlsym(handle, "___header"); + } + + return symbol; +} + +void lc_close(void *handle) +{ + if (handle) { + dlclose(handle); + } +} diff --git a/firmware/target/hosted/ctru/lc-program-resolver.c b/firmware/target/hosted/ctru/lc-program-resolver.c new file mode 100644 index 0000000000..5c504b1069 --- /dev/null +++ b/firmware/target/hosted/ctru/lc-program-resolver.c @@ -0,0 +1,53 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include "debug.h" + +#define RESOLVER_ENTRY(name) \ + if (!strcmp(symName, #name)) \ + return (void *)name; + +/* void* ctrdlProgramResolver(const char* symName) */ +void* programResolver(const char* symName, void *userData) +{ + DEBUGF("programResolver(name=\"%s\")\n", symName); + + /* codecs, etc */ + RESOLVER_ENTRY(abs); + RESOLVER_ENTRY(labs); + RESOLVER_ENTRY(llabs); + RESOLVER_ENTRY(printf); + + /* plugins */ + RESOLVER_ENTRY(_ctype_); + RESOLVER_ENTRY(__errno); + RESOLVER_ENTRY(longjmp); + RESOLVER_ENTRY(setjmp); + + return NULL; + (void) userData; +} diff --git a/firmware/target/hosted/ctru/lcd-bitmap.c b/firmware/target/hosted/ctru/lcd-bitmap.c new file mode 100644 index 0000000000..571857b9f3 --- /dev/null +++ b/firmware/target/hosted/ctru/lcd-bitmap.c @@ -0,0 +1,245 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2006 Dan Everton + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include "debug.h" +#include "system.h" +#include "button-ctru.h" +#include "screendump.h" +#include "lcd-target.h" + +#include <3ds/gfx.h> +#include <3ds/allocator/linear.h> +#include <3ds/console.h> +#include <3ds/services/cfgu.h> + +/*#define LOGF_ENABLE*/ +#include "logf.h" + +fb_data *dev_fb = 0; + +static u8 system_model = CFG_MODEL_3DS; + +typedef struct +{ + int width, height; +} dimensions_t; + +int get_dest_offset(int x, int y, int dest_width) +{ + return dest_width - y - 1 + dest_width * x; +} + +int get_source_offset(int x, int y, int source_width) +{ + return x + y * source_width; +} + +void copy_framebuffer_16(u16 *dest, const dimensions_t dest_dim, const u16 *source, const dimensions_t source_dim) +{ + int rows = MIN(dest_dim.width, source_dim.height); + int cols = MIN(dest_dim.height, source_dim.width); + for (int y = 0; y < rows; ++y) { + for (int x = 0; x < cols; ++x) { + const u16 *s = source + get_source_offset(x, y, source_dim.width); + u16 *d = dest + get_dest_offset(x, y, dest_dim.width); + *d = *s; + } + } +} + +void copy_framebuffer_24(u8 *dest, const dimensions_t dest_dim, const u8 *source, const dimensions_t source_dim) +{ + int rows = MIN(dest_dim.width, source_dim.height); + int cols = MIN(dest_dim.height, source_dim.width); + for (int y = 0; y < rows; ++y) { + for (int x = 0; x < cols; ++x) { + const u8 *s = source + get_source_offset(x, y, source_dim.width) * 3; + u8 *d = dest + get_dest_offset(x, y, dest_dim.width) * 3; + d[0] = s[0]; + d[1] = s[1]; + d[2] = s[2]; + } + } +} + +void copy_framebuffer_32(u32 *dest, const dimensions_t dest_dim, const u32 *source, const dimensions_t source_dim) +{ + int rows = MIN(dest_dim.width, source_dim.height); + int cols = MIN(dest_dim.height, source_dim.width); + for (int y = 0; y < rows; ++y) { + for (int x = 0; x < cols; ++x) { + const u32 *s = source + get_source_offset(x, y, source_dim.width); + u32 *d = dest + get_dest_offset(x, y, dest_dim.width); + *d = *s; + } + } +} + +void update_framebuffer(void) +{ + u16 fb_width, fb_height; + u32 bufsize; + u8* fb = gfxGetFramebuffer(GFX_BOTTOM, GFX_LEFT, &fb_width, &fb_height); + bufsize = fb_width * fb_height * 2; + + /* lock_window_mutex() */ + + if (FB_DATA_SZ == 2) { + copy_framebuffer_16((void *)fb, (dimensions_t){ fb_width, fb_height }, + (u16 *)dev_fb, (dimensions_t){ LCD_WIDTH, LCD_HEIGHT }); + } + else if (FB_DATA_SZ == 3) { + copy_framebuffer_24((void *)fb, (dimensions_t){ fb_width, fb_height }, + (u8 *)dev_fb, (dimensions_t){ LCD_WIDTH, LCD_HEIGHT }); + } + else { + copy_framebuffer_32((void *)fb, (dimensions_t){ fb_width, fb_height }, + (u32 *)dev_fb, (dimensions_t){ LCD_WIDTH, LCD_HEIGHT }); + } + + // Flush and swap framebuffers + /* gfxFlushBuffers(); */ + GSPGPU_FlushDataCache(fb, bufsize); + gfxSwapBuffers(); + gspWaitForVBlank(); + + /* unlock_window_mutex() */ +} + +/* lcd-as-memframe.c */ +extern void lcd_copy_buffer_rect(fb_data *dst, const fb_data *src, + int width, int height); + +void lcd_update_rect(int x, int y, int width, int height) +{ + fb_data *dst, *src; + + if (x + width > LCD_WIDTH) + width = LCD_WIDTH - x; /* Clip right */ + if (x < 0) + width += x, x = 0; /* Clip left */ + if (width <= 0) + return; /* nothing left to do */ + + if (y + height > LCD_HEIGHT) + height = LCD_HEIGHT - y; /* Clip bottom */ + if (y < 0) + height += y, y = 0; /* Clip top */ + if (height <= 0) + return; /* nothing left to do */ + + dst = LCD_FRAMEBUF_ADDR(x, y); + src = FBADDR(x,y); + + /* Copy part of the Rockbox framebuffer to the second framebuffer */ + if (width < LCD_WIDTH) + { + /* Not full width - do line-by-line */ + lcd_copy_buffer_rect(dst, src, width, height); + } + else + { + /* Full width - copy as one line */ + lcd_copy_buffer_rect(dst, src, LCD_WIDTH*height, 1); + } + + update_framebuffer(); +} + +void lcd_update(void) +{ + /* update a full screen rect */ + lcd_update_rect(0, 0, LCD_WIDTH, LCD_HEIGHT); +} + +void sys_console_init(void) +{ + gfxInit(GSP_BGR8_OES, GSP_RGB565_OES, false); + consoleInit(GFX_TOP, NULL); +} + +void lcd_init_device(void) +{ + gfxSetDoubleBuffering(GFX_BOTTOM, true); + + /* hidInit(); */ + + u16 fb_width, fb_height; + u8* fb = gfxGetFramebuffer(GFX_BOTTOM, GFX_LEFT, &fb_width, &fb_height); + u32 bufsize = fb_width * fb_height * 2; + + dev_fb = (fb_data *) linearAlloc(bufsize); + if (dev_fb == NULL) { + logf("could not allocate dev_fb size %d bytes\n", + bufsize); + exit(EXIT_FAILURE); + } + + memset(dev_fb, 0x00, bufsize); + + /* Set dpi value from system model */ + CFGU_GetSystemModel(&system_model); +} + +void lcd_shutdown(void) +{ + if (dev_fb) { + linearFree(dev_fb); + dev_fb = 0; + } + + /* hidExit(); */ + gfxExit(); +} + +int lcd_get_dpi(void) +{ + /* link: https://www.reddit.com/r/nintendo/comments/2uzk5y/informative_post_about_dpi_on_the_new_3ds/ */ + /* all values for bottom lcd */ + float dpi = 96.0f; + switch(system_model) { + case CFG_MODEL_3DS: + dpi = 132.45f; + break; + case CFG_MODEL_3DSXL: + dpi = 95.69f; + break; + case CFG_MODEL_N3DS: + dpi = 120.1f; + break; + case CFG_MODEL_2DS: + dpi = 132.45f; + break; + case CFG_MODEL_N3DSXL: + dpi = 95.69f; + break; + case CFG_MODEL_N2DSXL: + dpi = 95.69f; + break; + default: + break; + }; + return (int) roundf(dpi); +} + diff --git a/firmware/target/hosted/ctru/lcd-bitmap.h b/firmware/target/hosted/ctru/lcd-bitmap.h new file mode 100644 index 0000000000..ba3bcfcc70 --- /dev/null +++ b/firmware/target/hosted/ctru/lcd-bitmap.h @@ -0,0 +1,31 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __LCDBITMAP_H__ +#define __LCDBITMAP_H__ + +#include "lcd.h" + +void sim_lcd_init(void); +void sys_console_init(void); + +#endif /* #ifndef __LCDBITMAP_H__ */ + diff --git a/firmware/target/hosted/ctru/lcd-target.h b/firmware/target/hosted/ctru/lcd-target.h new file mode 100644 index 0000000000..4b1c1a4315 --- /dev/null +++ b/firmware/target/hosted/ctru/lcd-target.h @@ -0,0 +1,34 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef LCD_TARGET_H +#define LCD_TARGET_H + +#include "lcd.h" + +/* + Framebuffer device and framebuffer access. + See lcd-memframe.c +*/ +extern fb_data *dev_fb; +#define LCD_FRAMEBUF_ADDR(col, row) (dev_fb + row * LCD_WIDTH + col) + +#endif /* LCD_TARGET_H */ + diff --git a/firmware/target/hosted/ctru/lib/bfile/bfile-internal.h b/firmware/target/hosted/ctru/lib/bfile/bfile-internal.h new file mode 100644 index 0000000000..e274daf5f5 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/bfile/bfile-internal.h @@ -0,0 +1,99 @@ +#ifndef _BFILE_INTERNAL_H_ +#define _BFILE_INTERNAL_H_ + +#include +#include +#include +#include +#include +#include + +#include <3ds/gfx.h> +#include <3ds/svc.h> +#include <3ds/types.h> +#include <3ds/thread.h> +#include <3ds/result.h> +#include <3ds/services/fs.h> +#include <3ds/synchronization.h> + +/* #define MALLOC_DEBUG +#include "rmalloc/rmalloc.h" */ + +#include "cslice.h" +#include "cmap.h" + +#define nil NULL + +/* in go functions can return two values */ +#define two_type_value(type1, type2, name1, name2, type_name) \ +typedef struct { \ + type1 name1; \ + type2 name2; \ +} type_name##_t; + +/* single mutex implementation */ +#define sync_Mutex LightLock +static inline void sync_MutexInit(sync_Mutex* m) { + LightLock_Init(m); +} + +/* mutex unlock */ +static inline void sync_MutexLock(sync_Mutex* m) { + LightLock_Lock(m); +} + +/* mutex lock */ +static inline void sync_MutexUnlock(sync_Mutex* m) { + LightLock_Unlock(m); +} + +/* read_write mutex implementation */ +typedef struct { + sync_Mutex shared; + CondVar reader_q; + CondVar writer_q; + int active_readers; + int active_writers; + int waiting_writers; +} sync_RWMutex; + +void sync_RWMutexInit(sync_RWMutex *m); +void sync_RWMutexRLock(sync_RWMutex *m); +void sync_RWMutexRUnlock(sync_RWMutex *m); +void sync_RWMutexLock(sync_RWMutex *m); +void sync_RWMutexUnlock(sync_RWMutex *m); + +/* declare a two type value with name 'n_err_t' */ +two_type_value(int, const char*, n, err, int_error); +two_type_value(struct stat, const char*, fi, err, stat_error); +typedef const char* file_error_t; + +typedef struct page { + s64 num; + struct page* prev; + struct page* next; + u8* data; +} page; + +/* the two map types used by this library */ +cmap_declare(page, s64, struct page*); +cmap_declare(bool, s64, bool); + +typedef struct shard { + sync_Mutex mu; + cmap(page) pages; + cmap(bool) dirty; + struct page* head; + struct page* tail; +} shard; + +typedef struct Pager { + Handle file; + s64 pgsize; + s64 pgmax; + /* sync_RWMutex mu; */ + s64 size; + cslice(shard) shards; +} Pager; + +#endif /* _BFILE_INTERNAL_H_ */ diff --git a/firmware/target/hosted/ctru/lib/bfile/bfile.c b/firmware/target/hosted/ctru/lib/bfile/bfile.c new file mode 100644 index 0000000000..8542cf3ae0 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/bfile/bfile.c @@ -0,0 +1,471 @@ +/* + * This code is based on bfile.go by Josh Baker. + * Converted to C code by Mauricio G. + * Released under the MIT License. + */ + +/* IMPORTANT: this code only works for O_RDONLY and O_RDWR files. */ + +#include "bfile.h" + +/* note: for ease of reading and ease of comparing go code + with c implementation, function names are similar to the + go version */ + +/* note2: sync_RWMutex calls have been moved to rockbox sys_file + implementation. To use as standalone code please uncomment those calls. */ + +void sync_RWMutexInit(sync_RWMutex *m) { + LightLock_Init(&m->shared); + CondVar_Init(&m->reader_q); + CondVar_Init(&m->writer_q); + m->active_readers = 0; + m->active_writers = 0; + m->waiting_writers = 0; +} + +static inline LightLock *unique_lock(LightLock *lk) { + LightLock_Lock(lk); + return lk; +} + +void sync_RWMutexRLock(sync_RWMutex *m) { + LightLock *lk = unique_lock(&m->shared); + while (m->waiting_writers != 0) { + CondVar_Wait(&m->reader_q, lk); + } + ++m->active_readers; + LightLock_Unlock(lk); +} + +void sync_RWMutexRUnlock(sync_RWMutex *m) { + LightLock *lk = unique_lock(&m->shared); + --m->active_readers; + LightLock_Unlock(lk); + CondVar_Signal(&m->writer_q); +} + +void sync_RWMutexLock(sync_RWMutex *m) { + LightLock *lk = unique_lock(&m->shared); + ++m->waiting_writers; + while ((m->active_readers != 0) || (m->active_writers != 0)) { + CondVar_Wait(&m->writer_q, lk); + } + ++m->active_writers; + LightLock_Unlock(lk); +} + +void sync_RWMutexUnlock(sync_RWMutex *m) { + LightLock *lk = unique_lock(&m->shared); + --m->waiting_writers; + --m->active_writers; + if (m->waiting_writers > 0) { + CondVar_Signal(&m->writer_q); + } else { + CondVar_Broadcast(&m->reader_q); + } + LightLock_Unlock(lk); +} + +void s_init(shard* s); + +void s_push(shard* s, page* p) { + s->head->next->prev = p; + p->next = s->head->next; + p->prev = s->head; + s->head->next = p; +} + +void s_pop(shard* s, page* p) { + p->prev->next = p->next; + p->next->prev = p->prev; +} + +void s_bump(shard* s, page* p) { + s_pop(s, p); + s_push(s, p); +} + +/* page_pair_t destructor */ +/* page_pair_t type is defined by cmap_declare(page, s64, struct page*) */ +/* struct { + s64 key; + struct page* value; + } page_pair_t; +*/ +void page_pair_free(void* pair_ptr) { + if (pair_ptr) { + page_pair_t *pair = (page_pair_t*) pair_ptr; + struct page *p = pair->value; + if (p != nil) { + if (p->data != nil) { + free(p->data); + } + free(p); + } + } +} + +/* shard destructor */ +void s_free(void* s_ptr) { + if (s_ptr) { + shard *s = (shard*) s_ptr; + if (s->pages != nil) { + cmap_set_elem_destructor(s->pages, page_pair_free); + cmap_clear(page, s->pages); + cmap_clear(bool, s->dirty); + free(s->head); + free(s->tail); + } + } +} + +Pager* NewPager(Handle file) { + return NewPagerSize(file, 0, 0); +} + +Pager* NewPagerSize(Handle file, int pageSize, int bufferSize) { + if (pageSize <= 0) { + pageSize = defaultPageSize; + } else if ((pageSize & 4095) != 0) { + // must be a power of two + int x = 1; + while (x < pageSize) { + x *= 2; + } + pageSize = x; + } + + if (bufferSize <= 0) { + bufferSize = defaultBufferSize; + } else if (bufferSize < pageSize) { + bufferSize = pageSize; + } + + Pager* f = (Pager*) malloc(sizeof(Pager)); + f->file = file; + f->size = -1; + f->pgsize = (s64) pageSize; + + /* sync_RWMutexInit(&f->mu); */ + + // calculate the max number of pages across all shards + s64 pgmax = (s64) bufferSize / f->pgsize; + if (pgmax < minPages) { + pgmax = minPages; + } + + // calculate how many shards are needed, power of 2 + s64 nshards = (s64) ceil((double) pgmax / (double) pagesPerShard); + if (nshards > maxShards) { + nshards = maxShards; + } + s64 x = 1; + while (x < nshards) { + x *= 2; + } + nshards = x; + + // calculate the max number of pages per shard + f->pgmax = (s64) floor((double) pgmax / (double) nshards); + cslice_make(f->shards, nshards, (shard) { 0 }); + + // initialize sync mutex + size_t i; + for (i = 0; i < cslice_len(f->shards); i++) { + sync_MutexInit(&f->shards[i].mu); + } + return f; +} + +static int_error_t read_at(Handle file, u8 *data, size_t data_len, off_t off) +{ + u32 read_bytes = 0; + if (R_FAILED(FSFILE_Read(file, &read_bytes, (u64) off, data, (u32) data_len))) { + return (int_error_t) { -1, "I/O error" }; + } + + /* io.EOF */ + if (read_bytes == 0) { + return (int_error_t) { 0, "io.EOF" }; + } + + return (int_error_t) { (int) read_bytes, nil }; +} + +static int_error_t write_at(Handle file, u8 *data, size_t data_len, off_t off) +{ + u32 written = 0; + if (R_FAILED(FSFILE_Write(file, &written, (u64) off, data, (u32) data_len, FS_WRITE_FLUSH))) { + return (int_error_t) { -1, "I/O error" }; + } + + /* I/O error */ + if ((written == 0) || (written < (u32) data_len)) { + return (int_error_t) { -1, "I/O error" }; + } + + return (int_error_t) { (int) written, nil }; +} + +static stat_error_t file_stat(Handle file) +{ + u64 size = 0; + struct stat fi = { 0 }; + if (R_FAILED(FSFILE_GetSize(file, &size))) { + fi.st_size = 0; + return (stat_error_t) { fi, "I/O error" }; + } + + fi.st_size = (off_t) size; + return (stat_error_t) { fi, nil }; +} + +void s_init(shard* s) +{ + if (s->pages == nil) { + s->pages = cmap_make(/*s64*/page); + s->dirty = cmap_make(/*s64*/bool); + s->head = (page*) malloc(sizeof(page)); + s->tail = (page*) malloc(sizeof(page)); + s->head->next = s->tail; + s->tail->prev = s->head; + } +} + +file_error_t f_write(Pager* f, page* p) { + s64 off = p->num * f->pgsize; + s64 end = f->pgsize; + if ((off + end) > f->size) { + end = f->size - off; + } + int_error_t __err = write_at(f->file, p->data, end, off); + if (__err.err != nil) { + return __err.err; + } + return nil; +} + +file_error_t f_read(Pager* f, page* p) { + int_error_t __err = read_at(f->file, p->data, f->pgsize, p->num * f->pgsize); + if ((__err.err != nil) && strcmp(__err.err, "io.EOF")) { + return "I/O error"; + } + + return nil; +} + +const char* f_incrSize(Pager* f, s64 end, bool write) +{ +#define defer(m) \ + sync_RWMutexUnlock(&f->mu); \ + sync_RWMutexRLock(&f->mu); + + /* sync_RWMutexRUnlock(&f->mu); + sync_RWMutexLock(&f->mu); */ + + if (f->size == -1) { + stat_error_t fi_err = file_stat(f->file); + if (fi_err.err != nil) { + /* defer(&f->mu); */ + return nil; + } + f->size = fi_err.fi.st_size; + } + if (write && (end > f->size)) { + f->size = end; + } + + /* defer(&f->mu); */ + return nil; +} + +int_error_t f_pio(Pager *f, u8 *b, size_t len_b, s64 pnum, s64 pstart, s64 pend, bool write); +int_error_t f_io(Pager *f, u8 *b, size_t len_b, s64 off, bool write) { + if (f == nil) { + return (int_error_t) { 0, "invalid argument" }; + } + bool eof = false; + s64 start = off, end = off + len_b; + if (start < 0) { + return (int_error_t) { 0, "negative offset" }; + } + + // Check the upper bounds of the input to the known file size. + // Increase the file size if needed. + /* sync_RWMutexRLock(&f->mu); */ + if (end > f->size) { + file_error_t err = f_incrSize(f, end, write); + if (err != nil) { + /* sync_RWMutexRUnlock(&f->mu); */ + return (int_error_t) { 0, err }; + } + if (!write && (end > f->size)) { + end = f->size; + if ((end - start) < 0) { + end = start; + } + eof = true; + len_b = end-start; /* b = b[:end-start] */ + } + } + /* sync_RWMutexRUnlock(&f->mu); */ + + // Perform the page I/O. + int total = 0; + while (len_b > 0) { + s64 pnum = off / f->pgsize; + s64 pstart = off & (f->pgsize - 1); + s64 pend = pstart + (s64) len_b; + if (pend > f->pgsize) { + pend = f->pgsize; + } + + int_error_t result = f_pio(f, b, pend - pstart, pnum, pstart, pend, write); + if (result.err != nil) { + return (int_error_t) { total, result.err }; + } + + off += (s64) result.n; + total += result.n; + b = &b[result.n]; len_b -= result.n; /* b = b[n:] */ + } + if (eof) { + return (int_error_t) { total, "io.EOF" }; + } + + return (int_error_t) { total, nil }; +} + +int_error_t f_pio(Pager *f, u8 *b, size_t len_b, s64 pnum, s64 pstart, s64 pend, bool write) { + /* printf("pio(%p, %d, %lld, %lld, %lld, %s)\n", b, len_b, pnum, pstart, pend, write == true ? "true" : "false"); */ + shard *s = &f->shards[pnum & (s64) (cslice_len(f->shards) - 1)]; + sync_MutexLock(&s->mu); + s_init(s); + page *p = cmap_get_ptr(page, s->pages, pnum); + if (p == nil) { + // Page does not exist in memory. + // Acquire a new one. + if (cmap_len(s->pages) == f->pgmax) { + // The buffer is at capacity. + // Evict lru page and hang on to it. + p = s->tail->prev; + s_pop(s, p); + cmap_delete(page, s->pages, p->num); + if (cmap_get(bool, s->dirty, p->num)) { + // dirty page. flush it now + file_error_t err = f_write(f, p); + if (err != nil) { + sync_MutexUnlock(&s->mu); + return (int_error_t) { 0, err }; + } + cmap_delete(bool, s->dirty, p->num); + } + // Clear the previous page memory for partial page writes for + // pages that are being partially written to. + if (write && ((pend - pstart) < f->pgsize)) { + memset(p->data, 0, f->pgsize); + } + } else { + // Allocate an entirely new page. + p = (page *) malloc(sizeof(page)); + p->data = (u8 *) malloc(f->pgsize); + } + p->num = pnum; + // Read contents of page from file for all read operations, and + // partial write operations. Ignore for full page writes. + if (!write || ((pend-pstart) < f->pgsize)) { + file_error_t err = f_read(f, p); + if (err != nil) { + sync_MutexUnlock(&s->mu); + return (int_error_t) { 0, err }; + } + } + // Add the newly acquired page to the list. + cmap_set(page, s->pages, p->num, p); + s_push(s, p); + } else { + // Bump the page to the front of the list. + s_bump(s, p); + } + if (write) { + memcpy(p->data + pstart, b, pend - pstart); + cmap_set(bool, s->dirty, pnum, true); + } else { + memcpy(b, p->data + pstart, pend - pstart); + } + sync_MutexUnlock(&s->mu); + return (int_error_t) { len_b, nil }; +} + +// Flush writes any unwritten buffered data to the underlying file. +file_error_t PagerFlush(Pager *f) { + if (f == nil) { + return "invalid argument"; + } + + /* sync_RWMutexLock(&f->mu); */ + for (size_t i = 0; i < cslice_len(f->shards); i++) { + cmap_iterator(bool) pnum; + if (f->shards[i].dirty != nil) { + for (pnum = cmap_begin(f->shards[i].dirty); + pnum != cmap_end(f->shards[i].dirty); pnum++) { + if (pnum->value == true) { + page *p = cmap_get_ptr(page, f->shards[i].pages, pnum->key); + if (p != nil) { + file_error_t err = f_write(f, p); + if (err != nil) { + /* sync_RWMutexUnlock(&f->mu); */ + return err; + } + } + cmap_set(bool, f->shards[i].dirty, pnum->key, false); + } + } + } + } + /* sync_RWMutexUnlock(&f->mu); */ + return nil; +} + +// ReadAt reads len(b) bytes from the File starting at byte offset off. +int_error_t PagerReadAt(Pager *f, u8 *b, size_t len_b, off_t off) { + return f_io(f, b, len_b, off, false); +} + +// WriteAt writes len(b) bytes to the File starting at byte offset off. +int_error_t PagerWriteAt(Pager *f, u8 *b, size_t len_b, off_t off) { + return f_io(f, b, len_b, off, true); +} + +file_error_t PagerTruncate(Pager *f, off_t length) { + if (f == nil) { + return "invalid argument"; + } + + /* flush unwritten changes to disk */ + PagerFlush(f); + + /* sync_RWMutexRLock(&f->mu); */ + /* set new file size */ + Handle handle = f->file; + Result res = FSFILE_SetSize(handle, (u64) length); + if (R_FAILED(res)) { + return "I/O error"; + } + /* sync_RWMutexRUnlock(&f->mu); */ + + /* FIXME: truncate only required pages. Remove all for now */ + PagerClear(f); + f = NewPager(handle); + return nil; +} + +void PagerClear(Pager *f) { + if (f) { + cslice_set_elem_destructor(f->shards, s_free); + cslice_clear(f->shards); + free(f); + } +} + diff --git a/firmware/target/hosted/ctru/lib/bfile/bfile.h b/firmware/target/hosted/ctru/lib/bfile/bfile.h new file mode 100644 index 0000000000..870a9248d0 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/bfile/bfile.h @@ -0,0 +1,37 @@ +#ifndef _BFILE_H_ +#define _BFILE_H_ + +#include "bfile-internal.h" + +static const int defaultPageSize = 4096; // all pages are this size +static const int defaultBufferSize = 0x800000; // default buffer size, 8 MB +static const int minPages = 4; // minimum total pages per file +static const int pagesPerShard = 32; // ideal number of pages per shard +static const int maxShards = 128; // maximum number of shards per file + +// NewPager returns a new Pager that is backed by the provided file. +Pager* NewPager(Handle file); + +// NewPagerSize returns a new Pager with a custom page size and buffer size. +// The bufferSize is the maximum amount of memory dedicated to individual +// pages. Setting pageSize and bufferSize to zero will use their defaults, +// which are 4096 and 8 MB respectively. Custom values are rounded up to the +// nearest power of 2. +Pager* NewPagerSize(Handle file, int pageSize, int bufferSize); + +// ReadAt reads len(b) bytes from the File starting at byte offset off. +int_error_t PagerReadAt(Pager *f, u8 *b, size_t len_b, off_t off); + +// WriteAt writes len(b) bytes to the File starting at byte offset off. +int_error_t PagerWriteAt(Pager *f, u8 *b, size_t len_b, off_t off); + +// Flush writes any unwritten buffered data to the underlying file. +file_error_t PagerFlush(Pager *f); + +// Truncates pager to specified length +file_error_t PagerTruncate(Pager *f, off_t length); + +// Free all memory associated to a Pager file +void PagerClear(Pager *f); + +#endif /* _B_FILE_H_ */ diff --git a/firmware/target/hosted/ctru/lib/cmap.h b/firmware/target/hosted/ctru/lib/cmap.h new file mode 100644 index 0000000000..faa1eff9f3 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/cmap.h @@ -0,0 +1,209 @@ +#ifndef CMAP_H_ +#define CMAP_H_ + +#define CVECTOR_LINEAR_GROWTH +#include "cvector.h" + +/* note: for ease of porting go code to c, many functions (macros) names + remain similar to the ones used by go */ + +/* note2: this is a very basic map implementation. It does not do any sorting, and only works for basic types (and pointers) that can be compared with the equality operator */ + +#define nil NULL + +#define cmap_elem_destructor_t cvector_elem_destructor_t + +/** + * @brief cmap_declare - The map type used in this library + * @param name - The name associated to a map type. + * @param key_type - The map pair key type. + * @param val_type - The map pair value type. + * @param compare_func - The function used to compare for key_type. Should return value < 0 when a < b, 0 when a == b and value > 0 when a > b. + */ +#define cmap_declare(name, key_type, val_type) \ +typedef struct { \ + key_type key; \ + val_type value; \ +} name##_pair_t; \ +\ +typedef struct { \ + cvector(name##_pair_t) tree; \ + cmap_elem_destructor_t elem_destructor; \ +} name##_map_t; \ +\ +static inline val_type name##_get_( \ + name##_map_t *this, const key_type key) \ +{ \ + if (this) { \ + size_t i; \ + for (i = 0; i < cvector_size(this->tree); i++) { \ + if (key == this->tree[i].key) { \ + return this->tree[i].value; \ + } \ + } \ + } \ + return 0; \ +} \ +\ +static inline val_type name##_get_ptr_( \ + name##_map_t *this, const key_type key) \ +{ \ + if (this) { \ + size_t i; \ + for (i = 0; i < cvector_size(this->tree); i++) { \ + if (key == this->tree[i].key) { \ + return this->tree[i].value; \ + } \ + } \ + } \ + return nil; \ +} \ +\ +static inline void name##_set_( \ + name##_map_t *this, const key_type key, val_type value) \ +{ \ + if (this) { \ + size_t i; \ + for (i = 0; i < cvector_size(this->tree); i++) { \ + if (key == this->tree[i].key) { \ + this->tree[i].value = value; \ + return; \ + } \ + } \ + name##_pair_t new_pair = (name##_pair_t) { key, value }; \ + cvector_push_back(this->tree, new_pair); \ + } \ +} \ +\ +static inline void name##_delete_( \ + name##_map_t *this, const key_type key) \ +{ \ + if (this) { \ + size_t i; \ + for (i = 0; i < cvector_size(this->tree); i++) { \ + if (key == this->tree[i].key) { \ + cvector_erase(this->tree, i); \ + return; \ + } \ + } \ + } \ +} \ +\ +static inline name##_map_t* name##_map_make_(void) \ +{ \ + name##_map_t *map = (name##_map_t*) malloc(sizeof(name##_map_t)); \ + if (map) { \ + map->tree = nil; \ + cvector_init(map->tree, 0, nil); \ + return map; \ + } \ + return nil; \ +} \ +\ +static inline void name##_clear_(name##_map_t *this) \ +{ \ + if (this) { \ + cvector_free(this->tree); \ + free(this); \ + } \ +} \ +\ + + +/** + * @brief cmap - The map type used in this library + * @param name - The name associated to a map type. + */ +#define cmap(name) name##_map_t * + +/** + * @brief cmap_make - creates a new map. Automatically initializes the map. + * @param name - the name asociated to the map type + * @return a pointer to a new map. + */ +#define cmap_make(name) name##_map_make_() + +/** + * @brief cmap_size - gets the current size of the map + * @param map_ptr - the map pointer + * @return the size as a size_t + */ +#define cmap_len(map_ptr) cvector_size(map_ptr->tree) + +/** + * @brief cmap_get - gets value associated to a key. + * @param name - the name asociated to the map type + * @param map_ptr - the map pointer + * @param key - the key to search for + * @return the value associated to a key + */ +#define cmap_get(name, map_ptr, key) name##_get_(map_ptr, key) + +/** + * @brief cmap_get-ptr - gets ptr_value associated to a key. Use it to avoid assigning a ptr to 0. + * @param name - the name asociated to the map type + * @param map_ptr - the map pointer + * @param key - the key to search for + * @return the value associated to a key + */ +#define cmap_get_ptr(name, map_ptr, key) name##_get_ptr_(map_ptr, key) + + +/** + * @brief cmap_set - sets value associated to a key. + * @param name - the name asociated to the map type + * @param map_ptr - the map pointer + * @param key - the key to search for + * @param value - the new value + * @return void + */ +#define cmap_set(name, map_ptr, key, val) name##_set_(map_ptr, key, val) + +/** + * @brief cmap_delete - deletes map entry associated to a key. + * @param name - the name asociated to the map type + * @param map_ptr - the map pointer + * @param key - the key to search for + * @return void + */ +#define cmap_delete(name, map_ptr, key) name##_delete_(map_ptr, key) + +/** + * @brief cmap_set_elem_destructor - set the element destructor function + * used to clean up removed elements. The map must NOT be NULL for this to do anything. + * @param map_ptr - the map pointer + * @param elem_destructor_fn - function pointer of type cvector_elem_destructor_t used to destroy elements + * @return void + */ +#define cmap_set_elem_destructor(map_ptr, elem_destructor_fn) \ + cvector_set_elem_destructor(map_ptr->tree, elem_destructor_fn) + +/** + * @brief cmap_clear - deletes all map entries. And frees memory is an element destructor was set previously. + * @param name - the name asociated to the map type + * @param map_ptr - the map pointer + * @return void + */ +#define cmap_clear(name, map_ptr) name##_clear_(map_ptr) + +/** + * @brief cmap_iterator - The iterator type used for cmap + * @param type The type of iterator to act on. + */ +#define cmap_iterator(name) cvector_iterator(name##_pair_t) + +/** + * @brief cmap_begin - returns an iterator to first element of the vector + * @param map_ptr - the map pointer + * @return a pointer to the first element (or NULL) + */ +#define cmap_begin(map_ptr) ((map_ptr) ? cvector_begin(map_ptr->tree) : nil) + +/** + * @brief cmap_end - returns an iterator to one past the last element of the vector + * @param map_ptrs - the map pointer + * @return a pointer to one past the last element (or NULL) + */ +#define cmap_end(map_ptr) ((map_ptr) ? cvector_end(map_ptr->tree) : nil) + +#endif /* CMAP_H_ */ diff --git a/firmware/target/hosted/ctru/lib/cslice.h b/firmware/target/hosted/ctru/lib/cslice.h new file mode 100644 index 0000000000..36ec6d6faa --- /dev/null +++ b/firmware/target/hosted/ctru/lib/cslice.h @@ -0,0 +1,63 @@ +#ifndef CSLICE_H_ +#define CSLICE_H_ + +#define CVECTOR_LINEAR_GROWTH +#include "cvector.h" + +/* note: for ease of porting go code to c, many functions (macros) names + remain similar to the ones used by go */ + +#define nil NULL + +/** + * @brief cslice - The slice type used in this library + * @param type The type of slice to act on. + */ +#define cslice(type) cvector(type) + +/** + * @brief cslice_make - creates a new slice. Automatically initializes the slice. + * @param slice - the slice + * @param count - new size of the slice + * @param value - the value to initialize new elements with + * @return void + */ +#define cslice_make(slice, capacity, value) \ + do { \ + slice = nil; \ + cvector_init(slice, capacity, nil); \ + cvector_resize(slice, capacity, value); \ + } while(0) + +/** + * @brief cslice_size - gets the current size of the slice + * @param slice - the slice + * @return the size as a size_t + */ +#define cslice_len(slice) cvector_size(slice) + +/** + * @brief cslice_capacity - gets the current capacity of the slice + * @param slice - the slice + * @return the capacity as a size_t + */ +#define cslice_cap(slice) cvector_capacity(slice) + +/** + * @brief cslice_set_elem_destructor - set the element destructor function + * used to clean up removed elements. The vector must NOT be NULL for this to do anything. + * @param slice - the slice + * @param elem_destructor_fn - function pointer of type cslice_elem_destructor_t used to destroy elements + * @return void + */ +#define cslice_set_elem_destructor(slice, elem_destructor_fn) \ + cvector_set_elem_destructor(slice, elem_destructor_fn) + +/** + * @brief cslice_free - frees all memory associated with the slice + * @param slice - the slice + * @return void + */ +#define cslice_clear(slice) cvector_free(slice) + +#endif /* CSLICE_H_ */ diff --git a/firmware/target/hosted/ctru/lib/cvector.h b/firmware/target/hosted/ctru/lib/cvector.h new file mode 100644 index 0000000000..5045025c73 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/cvector.h @@ -0,0 +1,549 @@ +#ifndef CVECTOR_H_ +#define CVECTOR_H_ +/** + * @copyright Copyright (c) 2015 Evan Teran, + * License: The MIT License (MIT) + * @brief cvector heap implemented using C library malloc() + * @file cvector.h + */ + +/* in case C library malloc() needs extra protection, + * allow these defines to be overridden. + */ +/* functions for allocation and deallocation need to correspond to each other, fall back to C library functions if not all are overridden */ +#if !defined(cvector_clib_free) || !defined(cvector_clib_malloc) || !defined(cvector_clib_calloc) || !defined(cvector_clib_realloc) +#ifdef cvector_clib_free +#undef cvector_clib_free +#endif +#ifdef cvector_clib_malloc +#undef cvector_clib_malloc +#endif +#ifdef cvector_clib_calloc +#undef cvector_clib_calloc +#endif +#ifdef cvector_clib_realloc +#undef cvector_clib_realloc +#endif +#include +#define cvector_clib_free free +#define cvector_clib_malloc malloc +#define cvector_clib_calloc calloc +#define cvector_clib_realloc realloc +#endif +/* functions independent of memory allocation */ +#ifndef cvector_clib_assert +#include /* for assert */ +#define cvector_clib_assert assert +#endif +#ifndef cvector_clib_memcpy +#include /* for memcpy */ +#define cvector_clib_memcpy memcpy +#endif +#ifndef cvector_clib_memmove +#include /* for memmove */ +#define cvector_clib_memmove memmove +#endif + +/* NOTE: Similar to C's qsort and bsearch, you will receive a T* + * for a vector of Ts. This means that you cannot use `free` directly + * as a destructor. Instead if you have for example a cvector_vector_type(int *) + * you will need to supply a function which casts `elem_ptr` to an `int**` + * and then does a free on what that pointer points to: + * + * ex: + * + * void free_int(void *p) { free(*(int **)p); } + */ +typedef void (*cvector_elem_destructor_t)(void *elem_ptr); + +typedef struct cvector_metadata_t { + size_t size; + size_t capacity; + cvector_elem_destructor_t elem_destructor; +} cvector_metadata_t; + +/** + * @brief cvector_vector_type - The vector type used in this library + * @param type The type of vector to act on. + */ +#define cvector_vector_type(type) type * + +/** + * @brief cvector - Syntactic sugar to retrieve a vector type + * @param type The type of vector to act on. + */ +#define cvector(type) cvector_vector_type(type) + +/** + * @brief cvector_iterator - The iterator type used for cvector + * @param type The type of iterator to act on. + */ +#define cvector_iterator(type) cvector_vector_type(type) + +/** + * @note you can also safely pass a pointer to a cvector iterator to a function + * but you have to update the pointer at the end to update the original + * iterator. + * example: + * void function( cvector_vector_type( type ) * p_it ) + * { + * cvector_vector_type( type ) it = *p_it; + * it ++; + * + * ... + * + * *p_it = it; + * } + */ + +/** + * @brief cvector_vector_type_ptr - helper to make code more "readable" + * @param type - the vector type pointer + */ +#define cvector_ptr_type(type) \ + cvector_vector_type(type) * + +/** + * @brief cvector_vector_ptr_get_iterator/set - helpers to make code more "readable" + * @param it - the vector iterator + * @param ptr - the vector pointer + */ +#define cvector_ptr_get_iterator(ptr) \ + *(ptr) + +#define cvector_ptr_set(ptr, it) \ + *(ptr) = it + +/** + * @brief cvector_vector_container_declare - defined a vector container type + */ +#define cvector_vector_container_declare(name, type) \ +struct cvector_vector_container_##name { \ + cvector_vector_type(type) vector; \ +} + +/** + * @brief cvector_vector_container - used to pass a cvector wrapped inside a container as a function parameter + */ +#define cvector_vector_container(name) \ + struct cvector_vector_container_##name + + +/** + * @brief cvector_vec_to_base - For internal use, converts a vector pointer to a metadata pointer + * @param vec - the vector + * @return the metadata pointer of the vector + * @internal + */ +#define cvector_vec_to_base(vec) \ + (&((cvector_metadata_t *)(void *)(vec))[-1]) + +/** + * @brief cvector_base_to_vec - For internal use, converts a metadata pointer to a vector pointer + * @param ptr - pointer to the metadata + * @return the vector + * @internal + */ +#define cvector_base_to_vec(ptr) \ + ((void *)&((cvector_metadata_t *)(ptr))[1]) + +/** + * @brief cvector_capacity - gets the current capacity of the vector + * @param vec - the vector + * @return the capacity as a size_t + */ +#define cvector_capacity(vec) \ + ((vec) ? cvector_vec_to_base(vec)->capacity : (size_t)0) + +/** + * @brief cvector_size - gets the current size of the vector + * @param vec - the vector + * @return the size as a size_t + */ +#define cvector_size(vec) \ + ((vec) ? cvector_vec_to_base(vec)->size : (size_t)0) + +/** + * @brief cvector_elem_destructor - get the element destructor function used + * to clean up elements + * @param vec - the vector + * @return the function pointer as cvector_elem_destructor_t + */ +#define cvector_elem_destructor(vec) \ + ((vec) ? cvector_vec_to_base(vec)->elem_destructor : NULL) + +/** + * @brief cvector_empty - returns non-zero if the vector is empty + * @param vec - the vector + * @return non-zero if empty, zero if non-empty + */ +#define cvector_empty(vec) \ + (cvector_size(vec) == 0) + +/** + * @brief cvector_reserve - Requests that the vector capacity be at least enough + * to contain n elements. If n is greater than the current vector capacity, the + * function causes the container to reallocate its storage increasing its + * capacity to n (or greater). + * @param vec - the vector + * @param n - Minimum capacity for the vector. + * @return void + */ +#define cvector_reserve(vec, n) \ + do { \ + size_t cv_reserve_cap__ = cvector_capacity(vec); \ + if (cv_reserve_cap__ < (n)) { \ + cvector_grow((vec), (n)); \ + } \ + } while (0) + +/** + * @brief cvector_init - Initialize a vector. The vector must be NULL for this to do anything. + * @param vec - the vector + * @param capacity - vector capacity to reserve + * @param elem_destructor_fn - element destructor function + * @return void + */ +#define cvector_init(vec, capacity, elem_destructor_fn) \ + do { \ + if (!(vec)) { \ + cvector_reserve((vec), capacity); \ + cvector_set_elem_destructor((vec), (elem_destructor_fn)); \ + } \ + } while (0) + +/** + * @brief cvector_init_default - Initialize a vector with default value. The vector must be NULL for this to do anything. Does NOT work for struct types. + * @param vec - the vector + * @param capacity - vector capacity to reserve + * @param elem_destructor_fn - element destructor function + * @return void + */ +#define cvector_init_default(vec, capacity, default) \ + do { \ + if (!(vec)) { \ + cvector_reserve((vec), capacity); \ + cvector_set_elem_destructor((vec), (elem_destructor_fn)); \ + } \ + } while (0) + +/** + * @brief cvector_erase - removes the element at index i from the vector + * @param vec - the vector + * @param i - index of element to remove + * @return void + */ +#define cvector_erase(vec, i) \ + do { \ + if (vec) { \ + const size_t cv_erase_sz__ = cvector_size(vec); \ + if ((i) < cv_erase_sz__) { \ + cvector_elem_destructor_t cv_erase_elem_dtor__ = cvector_elem_destructor(vec); \ + if (cv_erase_elem_dtor__) { \ + cv_erase_elem_dtor__(&(vec)[i]); \ + } \ + cvector_set_size((vec), cv_erase_sz__ - 1); \ + cvector_clib_memmove( \ + (vec) + (i), \ + (vec) + (i) + 1, \ + sizeof(*(vec)) * (cv_erase_sz__ - 1 - (i))); \ + } \ + } \ + } while (0) + +/** + * @brief cvector_clear - erase all of the elements in the vector + * @param vec - the vector + * @return void + */ +#define cvector_clear(vec) \ + do { \ + if (vec) { \ + cvector_elem_destructor_t cv_clear_elem_dtor__ = cvector_elem_destructor(vec); \ + if (cv_clear_elem_dtor__) { \ + size_t cv_clear_i__; \ + for (cv_clear_i__ = 0; cv_clear_i__ < cvector_size(vec); ++cv_clear_i__) { \ + cv_clear_elem_dtor__(&(vec)[cv_clear_i__]); \ + } \ + } \ + cvector_set_size(vec, 0); \ + } \ + } while (0) + +/** + * @brief cvector_free - frees all memory associated with the vector + * @param vec - the vector + * @return void + */ +#define cvector_free(vec) \ + do { \ + if (vec) { \ + void *cv_free_p__ = cvector_vec_to_base(vec); \ + cvector_elem_destructor_t cv_free_elem_dtor__ = cvector_elem_destructor(vec); \ + if (cv_free_elem_dtor__) { \ + size_t cv_free_i__; \ + for (cv_free_i__ = 0; cv_free_i__ < cvector_size(vec); ++cv_free_i__) { \ + cv_free_elem_dtor__(&(vec)[cv_free_i__]); \ + } \ + } \ + cvector_clib_free(cv_free_p__); \ + } \ + } while (0) + +/** + * @brief cvector_begin - returns an iterator to first element of the vector + * @param vec - the vector + * @return a pointer to the first element (or NULL) + */ +#define cvector_begin(vec) \ + (vec) + +/** + * @brief cvector_end - returns an iterator to one past the last element of the vector + * @param vec - the vector + * @return a pointer to one past the last element (or NULL) + */ +#define cvector_end(vec) \ + ((vec) ? &((vec)[cvector_size(vec)]) : NULL) + +/* user request to use linear growth algorithm */ +#ifdef CVECTOR_LINEAR_GROWTH + +/** + * @brief cvector_compute_next_grow - returns an the computed size in next vector grow + * size is increased by 1 + * @param size - current size + * @return size after next vector grow + */ +#define cvector_compute_next_grow(size) \ + ((size) + 1) + +#else + +/** + * @brief cvector_compute_next_grow - returns an the computed size in next vector grow + * size is increased by multiplication of 2 + * @param size - current size + * @return size after next vector grow + */ +#define cvector_compute_next_grow(size) \ + ((size) ? ((size) << 1) : 1) + +#endif /* CVECTOR_LINEAR_GROWTH */ + +/** + * @brief cvector_push_back - adds an element to the end of the vector + * @param vec - the vector + * @param value - the value to add + * @return void + */ +#define cvector_push_back(vec, value) \ + do { \ + size_t cv_push_back_cap__ = cvector_capacity(vec); \ + if (cv_push_back_cap__ <= cvector_size(vec)) { \ + cvector_grow((vec), cvector_compute_next_grow(cv_push_back_cap__)); \ + } \ + (vec)[cvector_size(vec)] = (value); \ + cvector_set_size((vec), cvector_size(vec) + 1); \ + } while (0) + +/** + * @brief cvector_insert - insert element at position pos to the vector + * @param vec - the vector + * @param pos - position in the vector where the new elements are inserted. + * @param val - value to be copied (or moved) to the inserted elements. + * @return void + */ +#define cvector_insert(vec, pos, val) \ + do { \ + size_t cv_insert_cap__ = cvector_capacity(vec); \ + if (cv_insert_cap__ <= cvector_size(vec)) { \ + cvector_grow((vec), cvector_compute_next_grow(cv_insert_cap__)); \ + } \ + if ((pos) < cvector_size(vec)) { \ + cvector_clib_memmove( \ + (vec) + (pos) + 1, \ + (vec) + (pos), \ + sizeof(*(vec)) * ((cvector_size(vec)) - (pos))); \ + } \ + (vec)[(pos)] = (val); \ + cvector_set_size((vec), cvector_size(vec) + 1); \ + } while (0) + +/** + * @brief cvector_pop_back - removes the last element from the vector + * @param vec - the vector + * @return void + */ +#define cvector_pop_back(vec) \ + do { \ + cvector_elem_destructor_t cv_pop_back_elem_dtor__ = cvector_elem_destructor(vec); \ + if (cv_pop_back_elem_dtor__) { \ + cv_pop_back_elem_dtor__(&(vec)[cvector_size(vec) - 1]); \ + } \ + cvector_set_size((vec), cvector_size(vec) - 1); \ + } while (0) + +/** + * @brief cvector_copy - copy a vector + * @param from - the original vector + * @param to - destination to which the function copy to + * @return void + */ +#define cvector_copy(from, to) \ + do { \ + if ((from)) { \ + cvector_grow(to, cvector_size(from)); \ + cvector_set_size(to, cvector_size(from)); \ + cvector_clib_memcpy((to), (from), cvector_size(from) * sizeof(*(from))); \ + } \ + } while (0) + +/** + * @brief cvector_swap - exchanges the content of the vector by the content of another vector of the same type + * @param vec - the original vector + * @param other - the other vector to swap content with + * @param type - the type of both vectors + * @return void + */ +#define cvector_swap(vec, other, type) \ + do { \ + if (vec && other) { \ + cvector_vector_type(type) cv_swap__ = vec; \ + vec = other; \ + other = cv_swap__; \ + } \ + } while (0) + +/** + * @brief cvector_set_capacity - For internal use, sets the capacity variable of the vector + * @param vec - the vector + * @param size - the new capacity to set + * @return void + * @internal + */ +#define cvector_set_capacity(vec, size) \ + do { \ + if (vec) { \ + cvector_vec_to_base(vec)->capacity = (size); \ + } \ + } while (0) + +/** + * @brief cvector_set_size - For internal use, sets the size variable of the vector + * @param vec - the vector + * @param _size - the new capacity to set + * @return void + * @internal + */ +#define cvector_set_size(vec, _size) \ + do { \ + if (vec) { \ + cvector_vec_to_base(vec)->size = (_size); \ + } \ + } while (0) + +/** + * @brief cvector_set_elem_destructor - set the element destructor function + * used to clean up removed elements. The vector must NOT be NULL for this to do anything. + * @param vec - the vector + * @param elem_destructor_fn - function pointer of type cvector_elem_destructor_t used to destroy elements + * @return void + */ +#define cvector_set_elem_destructor(vec, elem_destructor_fn) \ + do { \ + if (vec) { \ + cvector_vec_to_base(vec)->elem_destructor = (elem_destructor_fn); \ + } \ + } while (0) + +/** + * @brief cvector_grow - For internal use, ensures that the vector is at least `count` elements big + * @param vec - the vector + * @param count - the new capacity to set + * @return void + * @internal + */ +#define cvector_grow(vec, count) \ + do { \ + const size_t cv_grow_sz__ = (count) * sizeof(*(vec)) + sizeof(cvector_metadata_t); \ + if (vec) { \ + void *cv_grow_p1__ = cvector_vec_to_base(vec); \ + void *cv_grow_p2__ = cvector_clib_realloc(cv_grow_p1__, cv_grow_sz__); \ + cvector_clib_assert(cv_grow_p2__); \ + (vec) = cvector_base_to_vec(cv_grow_p2__); \ + } else { \ + void *cv_grow_p__ = cvector_clib_malloc(cv_grow_sz__); \ + cvector_clib_assert(cv_grow_p__); \ + (vec) = cvector_base_to_vec(cv_grow_p__); \ + cvector_set_size((vec), 0); \ + cvector_set_elem_destructor((vec), NULL); \ + } \ + cvector_set_capacity((vec), (count)); \ + } while (0) + +/** + * @brief cvector_shrink_to_fit - requests the container to reduce its capacity to fit its size + * @param vec - the vector + * @return void + */ +#define cvector_shrink_to_fit(vec) \ + do { \ + if (vec) { \ + const size_t cv_shrink_to_fit_sz__ = cvector_size(vec); \ + cvector_grow(vec, cv_shrink_to_fit_sz__); \ + } \ + } while (0) + +/** + * @brief cvector_at - returns a reference to the element at position n in the vector. + * @param vec - the vector + * @param n - position of an element in the vector. + * @return the element at the specified position in the vector. + */ +#define cvector_at(vec, n) \ + ((vec) ? (((int)(n) < 0 || (size_t)(n) >= cvector_size(vec)) ? NULL : &(vec)[n]) : NULL) + +/** + * @brief cvector_front - returns a reference to the first element in the vector. Unlike member cvector_begin, which returns an iterator to this same element, this function returns a direct reference. + * @param vec - the vector + * @return a reference to the first element in the vector container. + */ +#define cvector_front(vec) \ + ((vec) ? ((cvector_size(vec) > 0) ? cvector_at(vec, 0) : NULL) : NULL) + +/** + * @brief cvector_back - returns a reference to the last element in the vector.Unlike member cvector_end, which returns an iterator just past this element, this function returns a direct reference. + * @param vec - the vector + * @return a reference to the last element in the vector. + */ +#define cvector_back(vec) \ + ((vec) ? ((cvector_size(vec) > 0) ? cvector_at(vec, cvector_size(vec) - 1) : NULL) : NULL) + +/** + * @brief cvector_resize - resizes the container to contain count elements. + * @param vec - the vector + * @param count - new size of the vector + * @param value - the value to initialize new elements with + * @return void + */ +#define cvector_resize(vec, count, value) \ + do { \ + if (vec) { \ + size_t cv_resize_count__ = (size_t)(count); \ + size_t cv_resize_sz__ = cvector_vec_to_base(vec)->size; \ + if (cv_resize_count__ > cv_resize_sz__) { \ + cvector_reserve((vec), cv_resize_count__); \ + cvector_set_size((vec), cv_resize_count__); \ + do { \ + (vec)[cv_resize_sz__++] = (value); \ + } while (cv_resize_sz__ < cv_resize_count__); \ + } else { \ + while (cv_resize_count__ < cv_resize_sz__--) { \ + cvector_pop_back(vec); \ + } \ + } \ + } \ + } while (0) + +#endif /* CVECTOR_H_ */ diff --git a/firmware/target/hosted/ctru/lib/sys_dir.c b/firmware/target/hosted/ctru/lib/sys_dir.c new file mode 100644 index 0000000000..0b9a0300fb --- /dev/null +++ b/firmware/target/hosted/ctru/lib/sys_dir.c @@ -0,0 +1,403 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#define DIRFUNCTIONS_DEFINED +#include "config.h" +#include +#include +#include "debug.h" +#include "dir.h" +#include "pathfuncs.h" +#include "timefuncs.h" +#include "system.h" +#include "fs_defines.h" +#include "sys_file.h" + +#include <3ds/archive.h> +#include <3ds/util/utf.h> + +/* This file is based on firmware/common/dir.c */ + +/* Define LOGF_ENABLE to enable logf output in this file */ +// #define LOGF_ENABLE +#include "logf.h" + +/* structure used for open directory streams */ +static struct dirstr_desc +{ + struct filestr_base stream; /* basic stream info (first!) */ + struct dirent entry; /* current parsed entry information */ +} open_streams[MAX_OPEN_DIRS] = +{ + [0 ... MAX_OPEN_FILES-1] = { .stream = { .handle = 0 } } +}; + +extern FS_Archive sdmcArchive; + +/* check and return a struct dirstr_desc* from a DIR* */ +static struct dirstr_desc * get_dirstr(DIR *dirp) +{ + struct dirstr_desc *dir = (struct dirstr_desc *)dirp; + + if (!PTR_IN_ARRAY(open_streams, dir, MAX_OPEN_DIRS)) + dir = NULL; + else if (dir->stream.handle != 0) + return dir; + + int errnum; + + if (!dir) + { + errnum = EFAULT; + } + else + { + logf("dir #%d: dir not open\n", (int)(dir - open_streams)); + errnum = EBADF; + } + + errno = errnum; + return NULL; +} + +#define GET_DIRSTR(type, dirp) \ + ({ \ + file_internal_lock_##type(); \ + struct dirstr_desc *_dir = get_dirstr(dirp); \ + if (_dir) \ + FILESTR_LOCK(type, &_dir->stream); \ + else { \ + file_internal_unlock_##type(); \ +} \ + _dir; \ + }) + +/* release the lock on the dirstr_desc* */ +#define RELEASE_DIRSTR(type, dir) \ + ({ \ + FILESTR_UNLOCK(type, &(dir)->stream); \ + file_internal_unlock_##type(); \ + }) + + +/* find a free dir stream descriptor */ +static struct dirstr_desc * alloc_dirstr(void) +{ + for (unsigned int dd = 0; dd < MAX_OPEN_DIRS; dd++) + { + struct dirstr_desc *dir = &open_streams[dd]; + if (dir->stream.handle == 0) + return dir; + } + + logf("Too many dirs open\n"); + return NULL; +} + +u32 fs_error(void) { + u32 err; + FSUSER_GetSdmcFatfsError(&err); + return err; +} + +/* Initialize the base descriptor */ +static void filestr_base_init(struct filestr_base *stream) +{ + stream->cache = nil; + stream->handle = 0; + stream->size = 0; + LightLock_Init(&stream->mtx); +} + +/** POSIX interface **/ + +/* open a directory */ +DIR * ctru_opendir(const char *dirname) +{ + logf("opendir(dirname=\"%s\")\n", dirname); + + DIR *dirp = NULL; + file_internal_lock_WRITER(); + + int rc; + + struct dirstr_desc * const dir = alloc_dirstr(); + if (!dir) + FILE_ERROR(EMFILE, _RC); + + filestr_base_init(&dir->stream); + Result res = FSUSER_OpenDirectory(&dir->stream.handle, + sdmcArchive, + fsMakePath(PATH_ASCII, dirname)); + if (R_FAILED(res)) { + logf("Open failed: %lld\n", fs_error()); + FILE_ERROR(EMFILE, -1); + } + + dir->stream.size = 0; + dir->stream.flags = 0; + + /* we will use file path to implement ctru_samedir function */ + strcpy(dir->stream.path, dirname); + + dirp = (DIR *)dir; +file_error: + file_internal_unlock_WRITER(); + return dirp; +} + +/* close a directory stream */ +int ctru_closedir(DIR *dirp) +{ + int rc; + + file_internal_lock_WRITER(); + + /* needs to work even if marked "nonexistant" */ + struct dirstr_desc * const dir = (struct dirstr_desc *)dirp; + if (!PTR_IN_ARRAY(open_streams, dir, MAX_OPEN_DIRS)) + FILE_ERROR(EFAULT, -1); + + logf("closedir(dirname=\"%s\")\n", dir->stream.path); + + if (dir->stream.handle == 0) + { + logf("dir #%d: dir not open\n", (int)(dir - open_streams)); + FILE_ERROR(EBADF, -2); + } + + Result res = FSDIR_Close(dir->stream.handle); + if (R_FAILED(res)) + FILE_ERROR(ERRNO, -3); + + dir->stream.handle = 0; + dir->stream.path[0] = '\0'; + + rc = 0; +file_error: + file_internal_unlock_WRITER(); + return rc; +} + +void dirstr_entry_init(FS_DirectoryEntry *dirEntry, struct dirent *entry) +{ + /* clear */ + memset(entry, 0, sizeof(struct dirent)); + + /* attributes */ + if (dirEntry->attributes & FS_ATTRIBUTE_DIRECTORY) + entry->info.attr |= ATTR_DIRECTORY; + if (dirEntry->attributes & FS_ATTRIBUTE_HIDDEN) + entry->info.attr |= ATTR_HIDDEN; + if (dirEntry->attributes & FS_ATTRIBUTE_ARCHIVE) + entry->info.attr |= ATTR_ARCHIVE; + if (dirEntry->attributes & FS_ATTRIBUTE_READ_ONLY) + entry->info.attr |= ATTR_READ_ONLY; + + /* size */ + entry->info.size = dirEntry->fileSize; + + /* name */ + uint8_t d_name[0xA0 + 1]; + memset(d_name, '\0', 0xA0); + utf16_to_utf8(d_name, (uint16_t *) &dirEntry->name, 0xA0); + memcpy(entry->d_name, d_name, 0xA0); +} + +/* read a directory */ +struct dirent * ctru_readdir(DIR *dirp) +{ + struct dirstr_desc * const dir = GET_DIRSTR(READER, dirp); + if (!dir) + FILE_ERROR_RETURN(ERRNO, NULL); + + int rc; + struct dirent *res = NULL; + + logf("readdir(dirname=\"%s\")\n", dir->stream.path); + + u32 dataRead = 0; + FS_DirectoryEntry dirEntry; + Result result = FSDIR_Read(dir->stream.handle, + &dataRead, + 1, + &dirEntry); + if (R_FAILED(result)) + FILE_ERROR(EIO, _RC); + + if (dataRead == 0) { + /* directory end. return NULL value, no errno */ + res = NULL; + goto file_error; + } + + res = &dir->entry; + dirstr_entry_init(&dirEntry, res); + + /* time */ + char full_path[MAX_PATH+1]; + + if (!strcmp(PATH_ROOTSTR, dir->stream.path)) + snprintf(full_path, MAX_PATH, "%s%s", dir->stream.path, res->d_name); + else + snprintf(full_path, MAX_PATH, "%s/%s", dir->stream.path, res->d_name); + + u64 mtime; + archive_getmtime(full_path, &mtime); + + /* DEBUGF("archive_getmtime(%s): %lld\n", full_path, mtime); */ + + uint16_t dosdate, dostime; + dostime_localtime(mtime, &dosdate, &dostime); + res->info.wrtdate = dosdate; + res->info.wrttime = dostime; + +file_error: + RELEASE_DIRSTR(READER, dir); + return res; +} + +/* make a directory */ +int ctru_mkdir(const char *path) +{ + logf("mkdir(path=\"%s\")\n", path); + + int rc; + + file_internal_lock_WRITER(); + + Result res = FSUSER_CreateDirectory(sdmcArchive, + fsMakePath(PATH_ASCII, path), + 0); + if (R_FAILED(res)) + FILE_ERROR(ERRNO, -1); + + rc = 0; +file_error: + file_internal_unlock_WRITER(); + return rc; +} + +/* remove a directory */ +int ctru_rmdir(const char *name) +{ + logf("rmdir(name=\"%s\")\n", name); + + int rc; + + if (name) + { + /* path may not end with "." */ + const char *basename; + size_t len = path_basename(name, &basename); + if (basename[0] == '.' && len == 1) + { + logf("Invalid path; last component is \".\"\n"); + FILE_ERROR_RETURN(EINVAL, -9); + } + } + + file_internal_lock_WRITER(); + Result res = FSUSER_DeleteDirectory(sdmcArchive, + fsMakePath(PATH_ASCII, name)); + if (R_FAILED(res)) + FILE_ERROR(ERRNO, -1); + + rc = 0; +file_error: + file_internal_unlock_WRITER(); + return rc; +} + + +/** Extended interface **/ + +/* return if two directory streams refer to the same directory */ +int ctru_samedir(DIR *dirp1, DIR *dirp2) +{ + struct dirstr_desc * const dir1 = GET_DIRSTR(WRITER, dirp1); + if (!dir1) + FILE_ERROR_RETURN(ERRNO, -1); + + int rc = -2; + + struct dirstr_desc * const dir2 = get_dirstr(dirp2); + if (dir2) { + rc = strcmp(dir1->stream.path, dir2->stream.path) == 0 ? 1 : 0; + } + + RELEASE_DIRSTR(WRITER, dir1); + return rc; +} + +/* test directory existence (returns 'false' if a file) */ +bool ctru_dir_exists(const char *dirname) +{ + file_internal_lock_WRITER(); + + int rc; + + Handle handle; + Result res = FSUSER_OpenDirectory(&handle, + sdmcArchive, + fsMakePath(PATH_ASCII, dirname)); + if (R_FAILED(res)) { + logf("Directory not found: %ld\n", fs_error()); + FILE_ERROR(EMFILE, -1); + } + + rc = 0; +file_error: + if (rc == 0) { + FSDIR_Close(handle); + } + file_internal_unlock_WRITER(); + return rc == 0 ? true : false; +} + +/* get the portable info from the native entry */ +struct dirinfo dir_get_info(DIR *dirp, struct dirent *entry) +{ + int rc; + if (!dirp || !entry) + FILE_ERROR(EFAULT, _RC); + + if (entry->d_name[0] == '\0') + FILE_ERROR(ENOENT, _RC); + + if ((file_size_t)entry->info.size > FILE_SIZE_MAX) + FILE_ERROR(EOVERFLOW, _RC); + + return (struct dirinfo) + { + .attribute = entry->info.attr, + .size = entry->info.size, + .mtime = dostime_mktime(entry->info.wrtdate, entry->info.wrttime), + }; + +file_error: + return (struct dirinfo){ .attribute = 0 }; +} + +const char* ctru_root_realpath(void) +{ + /* Native only, for APP and SIM see respective filesystem-.c files */ + return PATH_ROOTSTR; /* rb_namespace.c */ +} diff --git a/firmware/target/hosted/ctru/lib/sys_file.c b/firmware/target/hosted/ctru/lib/sys_file.c new file mode 100644 index 0000000000..1df88282d6 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/sys_file.c @@ -0,0 +1,777 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#define RB_FILESYSTEM_OS +#include "config.h" +#include "system.h" +#include +#include +#include +#include +#include +#include "file.h" +#include "debug.h" +#include "string-extra.h" +#include "fs_defines.h" +#include "sys_file.h" + +/* This file is based on firmware/common/file.c */ + +/* Define LOGF_ENABLE to enable logf output in this file */ +// #define LOGF_ENABLE +#include "logf.h" + +/** + * These functions provide a roughly POSIX-compatible file I/O API. + * Important: the bufferio I/O library (bfile) used in the 3ds does not work + * with write-only files due to an internal limitation. + * So all files will be opened with the read flag by default. + */ + +/* structure used for open file descriptors */ +static struct filestr_desc +{ + struct filestr_base stream; /* basic stream info (first!) */ + file_size_t offset; /* current offset for stream */ + u64 *sizep; /* shortcut to file size in fileobj */ +} open_streams[MAX_OPEN_FILES] = +{ + [0 ... MAX_OPEN_FILES-1] = { .stream = { .cache = nil, .flags = 0 } } +}; + +extern FS_Archive sdmcArchive; + +/* check and return a struct filestr_desc* from a file descriptor number */ +static struct filestr_desc * get_filestr(int fildes) +{ + struct filestr_desc *file = &open_streams[fildes]; + + if ((unsigned int)fildes >= MAX_OPEN_FILES) + file = NULL; + else if (file->stream.cache != nil) + return file; + + logf("fildes %d: bad file number\n", fildes); + errno = (file && (file->stream.cache == nil)) ? ENXIO : EBADF; + return NULL; +} + +#define GET_FILESTR(type, fildes) \ + ({ \ + file_internal_lock_##type(); \ + struct filestr_desc * _file = get_filestr(fildes); \ + if (_file) \ + FILESTR_LOCK(type, &_file->stream); \ + else { \ + file_internal_unlock_##type(); \ + }\ + _file; \ + }) + +/* release the lock on the filestr_desc* */ +#define RELEASE_FILESTR(type, file) \ + ({ \ + FILESTR_UNLOCK(type, &(file)->stream); \ + file_internal_unlock_##type(); \ + }) + +/* find a free file descriptor */ +static int alloc_filestr(struct filestr_desc **filep) +{ + for (int fildes = 0; fildes < MAX_OPEN_FILES; fildes++) + { + struct filestr_desc *file = &open_streams[fildes]; + if (file->stream.cache == nil) + { + *filep = file; + return fildes; + } + } + + logf("Too many files open\n"); + return -1; +} + +/* check for file existence */ +int test_stream_exists_internal(const char *path) +{ + int rc; + bool is_dir = false; + + Handle handle; + Result res = FSUSER_OpenFile(&handle, + sdmcArchive, + fsMakePath(PATH_ASCII, path), + FS_OPEN_READ, + 0); + if (R_FAILED(res)) { + /* not a file, try to open a directory */ + res = FSUSER_OpenDirectory(&handle, + sdmcArchive, + fsMakePath(PATH_ASCII, path)); + if (R_FAILED(res)) { + logf("File does not exist\n"); + FILE_ERROR(ERRNO, -1); + } + + is_dir = true; + } + + rc = 1; +file_error: + if (handle > 0) { + if (is_dir) + FSDIR_Close(handle); + else + FSFILE_Close(handle); + } + + return rc; +} + +/* set the file pointer */ +static off_t lseek_internal(struct filestr_desc *file, off_t offset, + int whence) +{ + off_t rc; + off_t pos; + + off_t size = MIN(*file->sizep, FILE_SIZE_MAX); + off_t file_offset = AtomicLoad(&file->offset); + + switch (whence) + { + case SEEK_SET: + if (offset < 0 || (off_t)offset > size) + FILE_ERROR(EINVAL, -1); + + pos = offset; + break; + + case SEEK_CUR: + if ((offset < 0 && (off_t)-offset > file_offset) || + (offset > 0 && (off_t)offset > size - file_offset)) + FILE_ERROR(EINVAL, -1); + + pos = file_offset + offset; + break; + + case SEEK_END: + if (offset > 0 || (off_t)-offset > size) + FILE_ERROR(EINVAL, -1); + + pos = size + offset; + break; + + default: + FILE_ERROR(EINVAL, -1); + } + + AtomicSwap(&file->offset, pos); + + return pos; +file_error: + return rc; +} + +/* read from or write to the file; back end to read() and write() */ +static ssize_t readwrite(struct filestr_desc *file, void *buf, size_t nbyte, + bool write) +{ +#ifndef LOGF_ENABLE /* wipes out log before you can save it */ + /* DEBUGF("readwrite(%p,%lx,%lu,%s)\n", + file, (long)buf, (unsigned long)nbyte, write ? "write" : "read"); */ +#endif + + const file_size_t size = *file->sizep; + size_t filerem; + + if (write) + { + /* if opened in append mode, move pointer to end */ + if (file->stream.flags & O_APPEND) + AtomicSwap(&file->offset, MIN(size, FILE_SIZE_MAX)); + + filerem = FILE_SIZE_MAX - AtomicLoad(&file->offset); + } + else + { + /* limit to maximum possible offset (EOF or FILE_SIZE_MAX) */ + filerem = MIN(size, FILE_SIZE_MAX) - AtomicLoad(&file->offset); + } + + if (nbyte > filerem) + { + nbyte = filerem; + if (nbyte > 0) + {} + else if (write) + FILE_ERROR_RETURN(EFBIG, -1); /* would get too large */ + else if (AtomicLoad(&file->offset) >= FILE_SIZE_MAX) + FILE_ERROR_RETURN(EOVERFLOW, -2); /* can't read here */ + } + + if (nbyte == 0) + return 0; + + int rc = 0; + int_error_t n_err; + + if (write) + n_err = PagerWriteAt(file->stream.cache, (u8 *) buf, nbyte, AtomicLoad(&file->offset)); + else + n_err = PagerReadAt(file->stream.cache, (u8 *) buf, nbyte, AtomicLoad(&file->offset)); + + if ((n_err.err != nil) && strcmp(n_err.err, "io.EOF")) { + FILE_ERROR(ERRNO, -3); + } + +file_error:; +#ifdef DEBUG + if (errno == ENOSPC) + logf("No space left on device\n"); +#endif + + size_t done = n_err.n; + if (done) + { + /* error or not, update the file offset and size if anything was + transferred */ + AtomicAdd(&file->offset, done); +#ifndef LOGF_ENABLE /* wipes out log before you can save it */ + /* DEBUGF("file offset: %lld\n", file->offset); */ +#endif + /* adjust file size to length written */ + if (write && AtomicLoad(&file->offset) > size) + *file->sizep = AtomicLoad(&file->offset); + + return done; + } + + return rc; +} + +/* initialize the base descriptor */ +static void filestr_base_init(struct filestr_base *stream) +{ + stream->cache = nil; + stream->handle = 0; + stream->size = 0; + LightLock_Init(&stream->mtx); +} + +int open_internal_inner2(Handle *handle, const char *path, u32 openFlags, u32 attributes) +{ + int rc; + Result res = FSUSER_OpenFile(handle, + sdmcArchive, + fsMakePath(PATH_ASCII, path), + openFlags, + attributes); + if (R_FAILED(res)) { + FILE_ERROR(ERRNO, -1); + } + + rc = 1; +file_error: + return rc; +} + +static int open_internal_inner1(const char *path, int oflag) +{ + int rc; + struct filestr_desc *file; + int fildes = alloc_filestr(&file); + if (fildes < 0) + FILE_ERROR_RETURN(EMFILE, -1); + + u32 openFlags = 0, attributes = 0; + + /* open for reading by default */ + openFlags = FS_OPEN_READ; + + if (oflag & O_ACCMODE) + { + if ((oflag & O_ACCMODE) == O_RDONLY) { + attributes |= FS_ATTRIBUTE_READ_ONLY; + } + if ((oflag & O_ACCMODE) == O_WRONLY) { + openFlags |= FS_OPEN_WRITE; + } + if ((oflag & O_ACCMODE) == O_RDWR) { + openFlags |= FS_OPEN_WRITE; + } + } + else if (oflag & O_TRUNC) + { + /* O_TRUNC requires write mode */ + logf("No write mode but have O_TRUNC\n"); + FILE_ERROR(EINVAL, -2); + } + + /* O_CREAT and O_APPEND are fine without write mode + * for the former, an empty file is created but no data may be written + * for the latter, no append will be allowed anyway */ + if (!(oflag & O_CREAT)) + oflag &= ~O_EXCL; /* result is undefined: we choose "ignore" */ + + filestr_base_init(&file->stream); + rc = open_internal_inner2(&file->stream.handle, path, openFlags, attributes); + + if (rc > 0) { + if (oflag & O_EXCL) + { + logf("File exists\n"); + FILE_ERROR(EEXIST, -4); + } + } + else if (oflag & O_CREAT) + { + /* not found; try to create it */ + openFlags |= FS_OPEN_CREATE; + rc = open_internal_inner2(&file->stream.handle, path, openFlags, attributes); + if (rc < 0) + FILE_ERROR(ERRNO, rc * 10 - 6); + } + else + { + logf("File not found\n"); + FILE_ERROR(ENOENT, -5); + } + + /* truncate file if requested */ + if (oflag & O_TRUNC) { + Result res = FSFILE_SetSize(file->stream.handle, 0); + if (R_FAILED(res)) { + FILE_ERROR(ERRNO, -6); + } + } + + /* we need to set file size here, or else lseek + will fail if no read or write has been done */ + u64 size = 0; + Result res = FSFILE_GetSize(file->stream.handle, &size); + if (R_FAILED(res)) { + FILE_ERROR(ERRNO, -8); + } + + int pageSize = 4096; /* 4096 bytes */ + int bufferSize = 512 * 1024; /* 512 kB */ + + /* streamed file formats like flac and mp3 need very large page + sizes to avoid stuttering */ + if (((oflag & O_ACCMODE) == O_RDONLY) && (size > 0x200000)) { + /* printf("open(%s)_BIG_pageSize\n", path); */ + pageSize = 32 * 1024; + bufferSize = MIN(size, defaultBufferSize); + } + + file->stream.cache = NewPagerSize(file->stream.handle, + pageSize, + bufferSize); + if (file->stream.cache == nil) { + FILE_ERROR(ERRNO, -7); + } + + file->stream.flags = oflag; + file->stream.size = size; + file->sizep = &file->stream.size; + AtomicSwap(&file->offset, 0); + + /* we will use file path to implement ctru_fsamefile function */ + strcpy(file->stream.path, path); + + return fildes; + +file_error: + if (fildes >= 0) { + if (file->stream.cache != nil) { + PagerFlush(file->stream.cache); + PagerClear(file->stream.cache); + file->stream.cache = nil; + } + + FSFILE_Close(file->stream.handle); + file->stream.handle = 0; + } + + return rc; +} + +static int open_internal_locked(const char *path, int oflag) +{ + file_internal_lock_WRITER(); + int rc = open_internal_inner1(path, oflag); + file_internal_unlock_WRITER(); + return rc; +} + +int ctru_open(const char *path, int oflag, ...) +{ + logf("open(path=\"%s\",oflag=%X)\n", path, (unsigned)oflag); + return open_internal_locked(path, oflag); +} + +int ctru_creat(const char *path, mode_t mode) +{ + logf("creat(path=\"%s\")\n", path); + return ctru_open(path, O_WRONLY|O_CREAT|O_TRUNC, mode); +} + +int ctru_close(int fildes) +{ + logf("close(fd=%d)\n", fildes); + + int rc; + + file_internal_lock_WRITER(); + + /* needs to work even if marked "nonexistant" */ + struct filestr_desc *file = &open_streams[fildes]; + if ((unsigned int)fildes >= MAX_OPEN_FILES || (file->stream.cache == nil)) + { + logf("filedes %d not open\n", fildes); + FILE_ERROR(EBADF, -2); + } + + if (file->stream.cache != nil) { + PagerFlush(file->stream.cache); + PagerClear(file->stream.cache); + file->stream.cache = nil; + } + + FSFILE_Close(file->stream.handle); + file->stream.handle = 0; + file->stream.path[0] = '\0'; + + rc = 0; +file_error: + file_internal_unlock_WRITER(); + return rc; +} + +/* truncate a file to a specified length */ +int ctru_ftruncate(int fildes, off_t length) +{ + logf("ftruncate(fd=%d,len=%ld)\n", fildes, (long)length); + + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + int rc; + + if (file->stream.flags & O_RDONLY) + { + logf("Descriptor is read-only mode\n"); + FILE_ERROR(EBADF, -2); + } + + if (length < 0) + { + logf("Length %ld is invalid\n", (long)length); + FILE_ERROR(EINVAL, -3); + } + + file_error_t err = PagerTruncate(file->stream.cache, length); + if (err) { + FILE_ERROR(ERRNO, -11); + } + + *file->sizep = length; + + rc = 0; +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/* synchronize changes to a file */ +int ctru_fsync(int fildes) +{ + logf("fsync(fd=%d)\n", fildes); + + struct filestr_desc * const file = GET_FILESTR(WRITER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + int rc; + + if (file->stream.flags & O_RDONLY) + { + logf("Descriptor is read-only mode\n"); + FILE_ERROR(EINVAL, -2); + } + + /* flush all pending changes to disk */ + file_error_t err = PagerFlush(file->stream.cache); + if (err != nil) { + FILE_ERROR(ERRNO, -3); + } + + rc = 0; +file_error: + RELEASE_FILESTR(WRITER, file); + return rc; +} + +/* move the read/write file offset */ +off_t ctru_lseek(int fildes, off_t offset, int whence) +{ +#ifndef LOGF_ENABLE /* wipes out log before you can save it */ + /* DEBUGF("lseek(fd=%d,ofs=%ld,wh=%d)\n", fildes, (long)offset, whence); */ +#endif + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + off_t rc = lseek_internal(file, offset, whence); + if (rc < 0) + FILE_ERROR(ERRNO, rc * 10 - 2); + +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/* read from a file */ +ssize_t ctru_read(int fildes, void *buf, size_t nbyte) +{ + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + ssize_t rc; + + if (file->stream.flags & O_WRONLY) + { + logf("read(fd=%d,buf=%p,nb=%lu) - " + "descriptor is write-only mode\n", + fildes, buf, (unsigned long)nbyte); + FILE_ERROR(EBADF, -2); + } + + rc = readwrite(file, buf, nbyte, false); + if (rc < 0) + FILE_ERROR(ERRNO, rc * 10 - 3); + +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/* write on a file */ +ssize_t ctru_write(int fildes, const void *buf, size_t nbyte) +{ + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + ssize_t rc; + + if (file->stream.flags & O_RDONLY) + { + logf("write(fd=%d,buf=%p,nb=%lu) - " + "descriptor is read-only mode\n", + fildes, buf, (unsigned long)nbyte); + FILE_ERROR(EBADF, -2); + } + + rc = readwrite(file, (void *)buf, nbyte, true); + if (rc < 0) + FILE_ERROR(ERRNO, rc * 10 - 3); + +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/* remove a file */ +int ctru_remove(const char *path) +{ + logf("remove(path=\"%s\")\n", path); + + int rc; + + file_internal_lock_WRITER(); + Result res = FSUSER_DeleteFile(sdmcArchive, + fsMakePath(PATH_ASCII, path)); + if (R_FAILED(res)) + FILE_ERROR(ERRNO, -1); + + rc = 0; +file_error: + file_internal_unlock_WRITER(); + return rc; +} + +/* rename a file */ +int ctru_rename(const char *old, const char *new) +{ + /* note: move by rename does not work in devkitARM toolchain */ + logf("rename(old=\"%s\",new=\"%s\")\n", old, new); + + int rc; + + /* if 'old' is a directory then 'new' is also required to be one if 'new' + is to be overwritten */ + bool are_dirs = false; + + file_internal_lock_WRITER(); + + if (!strcmp(new, old)) /* case-only is ok */ + { + logf("No name change (success)\n"); + rc = 0; + FILE_ERROR(ERRNO, _RC); + } + + /* open 'old'; it must exist */ + Handle open1rc; + Result res = FSUSER_OpenFile(&open1rc, + sdmcArchive, + fsMakePath(PATH_ASCII, old), + FS_OPEN_READ, + 0); + if (R_FAILED(res)) { + /* not a file, try to open a directory */ + res = FSUSER_OpenDirectory(&open1rc, + sdmcArchive, + fsMakePath(PATH_ASCII, old)); + if (R_FAILED(res)) { + logf("Failed opening old\n"); + FILE_ERROR(ERRNO, -1); + } + + are_dirs = true; + } + + if (are_dirs) { + /* rename directory */ + FSUSER_RenameDirectory(sdmcArchive, + fsMakePath(PATH_ASCII, old), + sdmcArchive, + fsMakePath(PATH_ASCII, new)); + } + else { + /* rename file */ + FSUSER_RenameFile(sdmcArchive, + fsMakePath(PATH_ASCII, old), + sdmcArchive, + fsMakePath(PATH_ASCII, new)); + } + + if (R_FAILED(res)) { + logf("Rename failed\n"); + FILE_ERROR(ERRNO, -2); + } + + rc = 0; +file_error: + /* for now, there is nothing to fail upon closing the old stream */ + if (open1rc > 0) { + if (are_dirs) + FSDIR_Close(open1rc); + else + FSFILE_Close(open1rc); + } + + file_internal_unlock_WRITER(); + return rc; +} + +/** Extensions **/ + +/* todo: utime does not work in devkitARM toolchain */ +int ctru_modtime(const char *path, time_t modtime) +{ + struct utimbuf times = + { + .actime = modtime, + .modtime = modtime, + }; + + return utime(path, ×); +} + +/* get the binary size of a file (in bytes) */ +off_t ctru_filesize(int fildes) +{ + struct filestr_desc * const file = GET_FILESTR(READER, fildes); + if (!file) + FILE_ERROR_RETURN(ERRNO, -1); + + off_t rc; + file_size_t size = *file->sizep; + + if (size > FILE_SIZE_MAX) + FILE_ERROR(EOVERFLOW, -2); + + rc = (off_t)size; +file_error: + RELEASE_FILESTR(READER, file); + return rc; +} + +/* test if two file descriptors refer to the same file */ +int ctru_fsamefile(int fildes1, int fildes2) +{ + struct filestr_desc * const file1 = GET_FILESTR(WRITER, fildes1); + if (!file1) + FILE_ERROR_RETURN(ERRNO, -1); + + int rc = -2; + + struct filestr_desc * const file2 = get_filestr(fildes2); + if (file2) + rc = strcmp(file1->stream.path, file2->stream.path) == 0 ? 1 : 0; + + RELEASE_FILESTR(WRITER, file1); + return rc; +} + +/* tell the relationship of path1 to path2 */ +int ctru_relate(const char *path1, const char *path2) +{ + /* FAT32 file system does not support symbolic links, + therefore, comparing the two full paths should be enough + to tell relationship */ + logf("relate(path1=\"%s\",path2=\"%s\")\n", path1, path2); + int rc = RELATE_DIFFERENT; + if (strcmp(path1, path2) == 0) + rc = RELATE_SAME; + return rc; +} + +/* test file or directory existence */ +bool ctru_file_exists(const char *path) +{ + file_internal_lock_WRITER(); + bool rc = test_stream_exists_internal(path) > 0; + file_internal_unlock_WRITER(); + return rc; +} + +/* note: no symbolic links support in devkitARM */ +ssize_t ctru_readlink(const char *path, char *buf, size_t bufsiz) +{ + return readlink(path, buf, bufsiz); +} + diff --git a/firmware/target/hosted/ctru/lib/sys_file.h b/firmware/target/hosted/ctru/lib/sys_file.h new file mode 100644 index 0000000000..cfed6017fc --- /dev/null +++ b/firmware/target/hosted/ctru/lib/sys_file.h @@ -0,0 +1,129 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 by Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef _SYS_FILE_H +#define _SYS_FILE_H + +#include "bfile.h" + +/* Include for file.h and dir.h because mkdir and friends may be here */ +#include + +#define strlcpy_from_os strlcpy + +#define AtomicLoad(ptr) __atomic_load_n((u32*)(ptr), __ATOMIC_SEQ_CST) +#define AtomicAdd(ptr, value) __atomic_add_fetch((u32*)(ptr), value, __ATOMIC_SEQ_CST) + +struct filestr_base { + Pager* cache; /* buffer IO implementation (cache) */ + Handle handle; /* file handle */ + u64 size; /* file size */ + int flags; /* stream flags */ + char path[MAX_PATH+1]; + + LightLock mtx; /* serialization for this stream */ +}; + +static inline void filestr_lock(struct filestr_base *stream) +{ + LightLock_Lock(&stream->mtx); +} + +static inline void filestr_unlock(struct filestr_base *stream) +{ + LightLock_Unlock(&stream->mtx); +} + +/* stream lock doesn't have to be used if getting RW lock writer access */ +#define FILESTR_WRITER 0 +#define FILESTR_READER 1 + +#define FILESTR_LOCK(type, stream) \ + ({ if (FILESTR_##type) filestr_lock(stream); }) + +#define FILESTR_UNLOCK(type, stream) \ + ({ if (FILESTR_##type) filestr_unlock(stream); }) + +/** Synchronization used throughout **/ + +/* acquire the filesystem lock as READER */ +static inline void file_internal_lock_READER(void) +{ + extern sync_RWMutex file_internal_mrsw; + sync_RWMutexRLock(&file_internal_mrsw); +} + +/* release the filesystem lock as READER */ +static inline void file_internal_unlock_READER(void) +{ + extern sync_RWMutex file_internal_mrsw; + sync_RWMutexRUnlock(&file_internal_mrsw); +} + +/* acquire the filesystem lock as WRITER */ +static inline void file_internal_lock_WRITER(void) +{ + extern sync_RWMutex file_internal_mrsw; + sync_RWMutexLock(&file_internal_mrsw); +} + +/* release the filesystem lock as WRITER */ +static inline void file_internal_unlock_WRITER(void) +{ + extern sync_RWMutex file_internal_mrsw; + sync_RWMutexUnlock(&file_internal_mrsw); +} + +#define ERRNO 0 /* maintain errno value */ +#define _RC 0 /* maintain rc value */ + +/* NOTES: if _errno is a non-constant expression, it must set an error + * number and not return the ERRNO constant which will merely set + * errno to zero, not preserve the current value; if you must set + * errno to zero, set it explicitly, not in the macro + * + * if _rc is constant-expression evaluation to 'RC', then rc will + * NOT be altered; i.e. if you must set rc to zero, set it explicitly, + * not in the macro + */ + +#define FILE_SET_CODE(_name, _keepcode, _value) \ + ({ __builtin_constant_p(_value) ? \ + ({ if ((_value) != (_keepcode)) _name = (_value); }) : \ + ({ _name = (_value); }); }) + +/* set errno and rc and proceed to the "file_error:" label */ +#define FILE_ERROR(_errno, _rc) \ + ({ FILE_SET_CODE(errno, ERRNO, (_errno)); \ + FILE_SET_CODE(rc, _RC, (_rc)); \ + goto file_error; }) + +/* set errno and return a value at the point of invocation */ +#define FILE_ERROR_RETURN(_errno, _rc...) \ + ({ FILE_SET_CODE(errno, ERRNO, _errno); \ + return _rc; }) + +/* set errno and return code, no branching */ +#define FILE_ERROR_SET(_errno, _rc) \ + ({ FILE_SET_CODE(errno, ERRNO, (_errno)); \ + FILE_SET_CODE(rc, _RC, (_rc)); }) + +#endif /* _SYS_FILE_H */ + diff --git a/firmware/target/hosted/ctru/lib/sys_thread.c b/firmware/target/hosted/ctru/lib/sys_thread.c new file mode 100644 index 0000000000..4d6ba2ff99 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/sys_thread.c @@ -0,0 +1,403 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio Ga. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include "sys_thread.h" +#include "sys_timer.h" +#include "debug.h" +#include "logf.h" + +bool _AtomicCAS(u32 *ptr, int oldval, int newval) +{ + int expected = oldval; + int desired = newval; + return __atomic_compare_exchange(ptr, &expected, &desired, false, + __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); +} + +bool _AtomicTryLock(int *lock) +{ + int result; + asm volatile( + "ldrex %0, [%2] \n" + "teq %0, #0 \n" + "strexeq %0, %1, [%2] \n" + : "=&r"(result) + : "r"(1), "r"(lock) + : "cc", "memory" + ); + return result == 0; +} + +#define CPUPauseInstruction() asm volatile("yield" ::: "memory") +void AtomicLock(int *lock) +{ + int iterations = 0; + while (!_AtomicTryLock(lock)) { + if (iterations < 32) { + iterations++; + CPUPauseInstruction(); + } else { + sys_delay(0); + } + } +} + +void AtomicUnlock(int *lock) +{ + *lock = 0; +} + +/* Convert rockbox priority value to libctru value */ +int get_ctru_thread_priority(int priority) +{ + if ((priority == PRIORITY_REALTIME_1) || (priority == PRIORITY_REALTIME_2) || + (priority == PRIORITY_REALTIME_3) || (priority == PRIORITY_REALTIME_4) || + (priority == PRIORITY_REALTIME)) + return 0x18; + else if (priority == PRIORITY_BUFFERING) + return 0x18; /* Highest */ + else if ((priority == PRIORITY_USER_INTERFACE) || (priority == PRIORITY_RECORDING) || + (priority == PRIORITY_PLAYBACK)) + return 0x30; + else if (priority == PRIORITY_PLAYBACK_MAX) + return 0x2F; + else if (priority == PRIORITY_SYSTEM) + return 0x30; + else if (priority == PRIORITY_BACKGROUND) + return 0x3F; /* Lowest */ + else + return 0x30; +} + +static size_t get_thread_stack_size(size_t requested_size) +{ + if (requested_size == 0) { + return (80 * 1024); /* 80 kB */ + } + + return requested_size; +} + +static void thread_entry(void *arg) +{ + sys_run_thread((sysThread *)arg); + threadExit(0); +} + +int wait_on_semaphore_for(LightSemaphore *sem, u32 timeout) +{ + u64 stop_time = sys_get_ticks64() + timeout; + u64 current_time = sys_get_ticks64(); + while (current_time < stop_time) { + if (LightSemaphore_TryAcquire(sem, 1) == 0) { + return 0; + } + /* 100 microseconds seems to be the sweet spot */ + svcSleepThread(100000LL); + current_time = sys_get_ticks64(); + } + + /* If we failed, yield to avoid starvation on busy waits */ + svcSleepThread(1); + return 1; +} + +int sys_sem_try_wait(LightSemaphore *sem) +{ + if (LightSemaphore_TryAcquire(sem, 1) != 0) { + /* If we failed, yield to avoid starvation on busy waits */ + svcSleepThread(1); + return 1; + } + + return 0; +} + +int sys_sem_wait_timeout(LightSemaphore *sem, u32 timeout) +{ + if (timeout == (~(u32)0)) { + LightSemaphore_Acquire(sem, 1); + return 0; + } + + if (LightSemaphore_TryAcquire(sem, 1) != 0) { + return wait_on_semaphore_for(sem, timeout); + } + + return 0; +} + +int sys_sem_wait(LightSemaphore *sem) +{ + return sys_sem_wait_timeout(sem, (~(u32)0)); +} + +u32 sys_sem_value(LightSemaphore *sem) +{ + return sem->current_count; +} + +int sys_thread_id(void) +{ + u32 thread_ID = 0; + svcGetThreadId(&thread_ID, CUR_THREAD_HANDLE); + return (int)thread_ID; +} + +void sys_run_thread(sysThread *thread) +{ + void *userdata = thread->userdata; + int(* userfunc)(void *) = thread->userfunc; + + int *statusloc = &thread->status; + + /* Get the thread id */ + thread->threadid = sys_thread_id(); + + /* Run the function */ + *statusloc = userfunc(userdata); + + /* Mark us as ready to be joined (or detached) */ + if (!AtomicCAS(&thread->state, THREAD_STATE_ALIVE, THREAD_STATE_ZOMBIE)) { + /* Clean up if something already detached us. */ + if (AtomicCAS(&thread->state, THREAD_STATE_DETACHED, THREAD_STATE_CLEANED)) { + free(thread); + } + } +} + +sysThread *sys_create_thread(int(*fn)(void *), const char *name, const size_t stacksize, + void *data IF_PRIO(, int priority) IF_COP(, unsigned int core)) +{ + sys_ticks_init(); + + /* Allocate memory for the thread info structure */ + sysThread *thread = (sysThread *) calloc(1, sizeof(sysThread)); + if (thread == NULL) { + DEBUGF("sys_create_thread: could not allocate memory\n"); + return NULL; + } + thread->status = -1; + AtomicSet(&thread->state, THREAD_STATE_ALIVE); + + /* Set up the arguments for the thread */ + thread->userfunc = fn; + thread->userdata = data; + thread->stacksize = stacksize; + + int cpu = -1; + if (name && (strncmp(name, "tagcache", 8) == 0) && R_SUCCEEDED(APT_SetAppCpuTimeLimit(30))) { + cpu = 1; + printf("thread: %s, running in cpu 1\n", name); + } + + thread->handle = threadCreate(thread_entry, + thread, + get_thread_stack_size(stacksize), + get_ctru_thread_priority(priority), + cpu, + false); + + if (!thread->handle) { + DEBUGF("sys_create_thread: threadCreate failed\n"); + free(thread); + thread = NULL; + } + + /* Everything is running now */ + return thread; +} + +void sys_wait_thread(sysThread *thread, int *status) +{ + if (thread) { + Result res = threadJoin(thread->handle, U64_MAX); + + /* + Detached threads can be waited on, but should NOT be cleaned manually + as it would result in a fatal error. + */ + if (R_SUCCEEDED(res) && AtomicGet(&thread->state) != THREAD_STATE_DETACHED) { + threadFree(thread->handle); + } + if (status) { + *status = thread->status; + } + free(thread); + } +} + +void sys_detach_thread(sysThread *thread) +{ + if (!thread) { + return; + } + + /* Grab dibs if the state is alive+joinable. */ + if (AtomicCAS(&thread->state, THREAD_STATE_ALIVE, THREAD_STATE_DETACHED)) { + threadDetach(thread->handle); + } else { + /* all other states are pretty final, see where we landed. */ + const int thread_state = AtomicGet(&thread->state); + if ((thread_state == THREAD_STATE_DETACHED) || (thread_state == THREAD_STATE_CLEANED)) { + return; /* already detached (you shouldn't call this twice!) */ + } else if (thread_state == THREAD_STATE_ZOMBIE) { + sys_wait_thread(thread, NULL); /* already done, clean it up. */ + } else { + assert(0 && "Unexpected thread state"); + } + } +} + +int sys_set_thread_priority(sysThread *thread, int priority) +{ + Handle h = threadGetHandle(thread->handle); + int old_priority = priority; + Result res = svcSetThreadPriority(h, get_ctru_thread_priority(priority)); + if (R_SUCCEEDED(res)) { + return priority; + } + + return old_priority; +} + +/* sysCond */ +sysCond *sys_cond_create(void) +{ + sysCond *cond; + + cond = (sysCond *)malloc(sizeof(sysCond)); + if (cond) { + RecursiveLock_Init(&cond->lock); + LightSemaphore_Init(&cond->wait_sem, 0, ((s16)0x7FFF)); + LightSemaphore_Init(&cond->wait_done, 0, ((s16)0x7FFF)); + cond->waiting = cond->signals = 0; + } else { + DEBUGF("sys_cond_create: out of memory.\n");; + } + return cond; +} + +/* Destroy a condition variable */ +void sys_cond_destroy(sysCond *cond) +{ + if (cond) { + free(cond); + } +} + +/* Restart one of the threads that are waiting on the condition variable */ +int sys_cond_signal(sysCond *cond) +{ + if (!cond) { + DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); + return -1; + } + + /* If there are waiting threads not already signalled, then + signal the condition and wait for the thread to respond. + */ + RecursiveLock_Lock(&cond->lock); + if (cond->waiting > cond->signals) { + ++cond->signals; + LightSemaphore_Release(&cond->wait_sem, 1); + RecursiveLock_Unlock(&cond->lock); + LightSemaphore_Acquire(&cond->wait_done, 1); + } else { + RecursiveLock_Unlock(&cond->lock); + } + + return 0; +} + +/* Restart all threads that are waiting on the condition variable */ +int sys_cond_broadcast(sysCond *cond) +{ + if (!cond) { + DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); + return -1; + } + + /* If there are waiting threads not already signalled, then + signal the condition and wait for the thread to respond. + */ + RecursiveLock_Lock(&cond->lock); + if (cond->waiting > cond->signals) { + int i, num_waiting; + + num_waiting = (cond->waiting - cond->signals); + cond->signals = cond->waiting; + for (i = 0; i < num_waiting; ++i) { + LightSemaphore_Release(&cond->wait_sem, 1); + } + /* Now all released threads are blocked here, waiting for us. + Collect them all (and win fabulous prizes!) :-) + */ + RecursiveLock_Unlock(&cond->lock); + for (i = 0; i < num_waiting; ++i) { + LightSemaphore_Acquire(&cond->wait_done, 1); + } + } else { + RecursiveLock_Unlock(&cond->lock); + } + + return 0; +} + +int sys_cond_wait(sysCond *cond, RecursiveLock *mutex) +{ + if (!cond) { + DEBUGF("sys_cond_signal: Invalid param 'cond'\n"); + return -1; + } + + RecursiveLock_Lock(&cond->lock); + ++cond->waiting; + RecursiveLock_Unlock(&cond->lock); + + /* Unlock the mutex, as is required by condition variable semantics */ + RecursiveLock_Unlock(mutex); + + /* Wait for a signal */ + LightSemaphore_Acquire(&cond->wait_sem, 1); + + RecursiveLock_Lock(&cond->lock); + if (cond->signals > 0) { + /* We always notify the signal thread that we are done */ + LightSemaphore_Release(&cond->wait_done, 1); + + /* Signal handshake complete */ + --cond->signals; + } + --cond->waiting; + RecursiveLock_Unlock(&cond->lock); + + /* Lock the mutex, as is required by condition variable semantics */ + RecursiveLock_Lock(mutex); + + return 0; +} + diff --git a/firmware/target/hosted/ctru/lib/sys_thread.h b/firmware/target/hosted/ctru/lib/sys_thread.h new file mode 100644 index 0000000000..c951668993 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/sys_thread.h @@ -0,0 +1,88 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __SYSTHREAD_H__ +#define __SYSTHREAD_H__ + +#include "thread.h" + +#include <3ds/synchronization.h> +#include <3ds/thread.h> +#include <3ds/services/apt.h> + +/* Complementary atomic operations */ +bool _AtomicCAS(u32 *ptr, int oldval, int newval); +#define AtomicGet(ptr) __atomic_load_n((u32*)(ptr), __ATOMIC_SEQ_CST) +#define AtomicSet(ptr, value) AtomicSwap(ptr, value) +#define AtomicCAS(ptr, oldvalue, newvalue) _AtomicCAS((u32 *)(ptr), oldvalue, newvalue) +void AtomicLock(int *lock); +void AtomicUnlock(int *lock); + +/* This code was taken from SDL2 thread implementation */ + +enum thread_state_t +{ + THREAD_STATE_ALIVE, + THREAD_STATE_DETACHED, + THREAD_STATE_ZOMBIE, + THREAD_STATE_CLEANED, +}; + +typedef struct _thread +{ + int threadid; + Thread handle; + int status; + int state; + size_t stacksize; + int(* userfunc)(void *); + void *userdata; + void *data; +} sysThread; + +typedef struct _cond +{ + RecursiveLock lock; + int waiting; + int signals; + LightSemaphore wait_sem; + LightSemaphore wait_done; +} sysCond; + +int sys_sem_wait(LightSemaphore *sem); +int sys_sem_wait_timeout(LightSemaphore *sem, u32 timeout); +int sys_sem_try_wait(LightSemaphore *sem); +u32 sys_sem_value(LightSemaphore *sem); + +sysThread *sys_create_thread(int(*fn)(void *), const char *name, const size_t stacksize, + void *data IF_PRIO(, int priority) IF_COP(, unsigned int core)); +void sys_run_thread(sysThread *thread); +void sys_wait_thread(sysThread *thread, int *status); +int sys_thread_id(void); +int sys_set_thread_priority(sysThread *thread, int priority); + +sysCond *sys_cond_create(void); +void sys_cond_destroy(sysCond *cond); +int sys_cond_signal(sysCond *cond); +int sys_cond_broadcast(sysCond *cond); +int sys_cond_wait(sysCond *cond, RecursiveLock *mutex); +#endif /* #ifndef __SYSTHREAD_H__ */ + diff --git a/firmware/target/hosted/ctru/lib/sys_timer.c b/firmware/target/hosted/ctru/lib/sys_timer.c new file mode 100644 index 0000000000..a38d7ca032 --- /dev/null +++ b/firmware/target/hosted/ctru/lib/sys_timer.c @@ -0,0 +1,402 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2006 Dan Everton + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include +#include "debug.h" +#include "logf.h" + +#include <3ds/os.h> +#include "sys_thread.h" +#include "sys_timer.h" + +#define CACHELINE_SIZE 128 + +static bool ticks_started = false; +static u64 start_tick; + +#define NSEC_PER_MSEC 1000000ULL + +void sys_ticks_init(void) +{ + if (ticks_started) { + return; + } + ticks_started = true; + + start_tick = svcGetSystemTick(); +} + +void sys_ticks_quit(void) +{ + ticks_started = false; +} + +u64 sys_get_ticks64(void) +{ + u64 elapsed; + if (!ticks_started) { + sys_ticks_init(); + } + + elapsed = svcGetSystemTick() - start_tick; + return elapsed / CPU_TICKS_PER_MSEC; +} + +u32 sys_get_ticks(void) +{ + return (u32)(sys_get_ticks64() & 0xFFFFFFFF); +} + +void sys_delay(u32 ms) +{ + svcSleepThread(ms * NSEC_PER_MSEC); +} + +typedef struct _timer +{ + int timerID; + timer_callback_ptr callback; + void *param; + u32 interval; + u32 scheduled; + int canceled; + struct _timer *next; +} sysTimer; + +typedef struct _timer_map +{ + int timerID; + sysTimer *timer; + struct _timer_map *next; +} timerMap; + +/* The timers are kept in a sorted list */ +typedef struct +{ + /* Data used by the main thread */ + Thread thread; + int nextID; + timerMap *timermap; + RecursiveLock timermap_lock; + + /* Padding to separate cache lines between threads */ + char cache_pad[CACHELINE_SIZE]; + + /* Data used to communicate with the timer thread */ + int lock; + LightSemaphore sem; + sysTimer *pending; + sysTimer *freelist; + int active; + + /* List of timers - this is only touched by the timer thread */ + sysTimer *timers; +} timerData; + +static timerData timer_data = { .active = 0 }; + +/* The idea here is that any thread might add a timer, but a single + * thread manages the active timer queue, sorted by scheduling time. + * + * Timers are removed by simply setting a canceled flag + */ + +static void add_timer_interval(timerData *data, sysTimer *timer) +{ + sysTimer *prev, *curr; + + prev = NULL; + for (curr = data->timers; curr; prev = curr, curr = curr->next) { + if ((s32)(timer->scheduled - curr->scheduled) < 0) { + break; + } + } + + /* Insert the timer here! */ + if (prev) { + prev->next = timer; + } else { + data->timers = timer; + } + timer->next = curr; +} + +static void timer_thread(void *_data) +{ + timerData *data = (timerData *)_data; + sysTimer *pending; + sysTimer *current; + sysTimer *freelist_head = NULL; + sysTimer *freelist_tail = NULL; + u32 tick, now, interval, delay; + + /* Threaded timer loop: + * 1. Queue timers added by other threads + * 2. Handle any timers that should dispatch this cycle + * 3. Wait until next dispatch time or new timer arrives + */ + for (;;) { + /* Pending and freelist maintenance */ + AtomicLock(&data->lock); + { + /* Get any timers ready to be queued */ + pending = data->pending; + data->pending = NULL; + + /* Make any unused timer structures available */ + if (freelist_head) { + freelist_tail->next = data->freelist; + data->freelist = freelist_head; + } + } + AtomicUnlock(&data->lock); + + /* Sort the pending timers into our list */ + while (pending) { + current = pending; + pending = pending->next; + add_timer_interval(data, current); + } + freelist_head = NULL; + freelist_tail = NULL; + + /* Check to see if we're still running, after maintenance */ + if (!AtomicGet(&data->active)) { + break; + } + + /* Initial delay if there are no timers */ + delay = (~(u32)0); + + tick = sys_get_ticks(); + + /* Process all the pending timers for this tick */ + while (data->timers) { + current = data->timers; + + if ((s32)(tick - current->scheduled) < 0) { + /* Scheduled for the future, wait a bit */ + delay = (current->scheduled - tick); + break; + } + + /* We're going to do something with this timer */ + data->timers = current->next; + + if (AtomicGet(¤t->canceled)) { + interval = 0; + } else { + interval = current->callback(current->interval, current->param); + } + + if (interval > 0) { + /* Reschedule this timer */ + current->interval = interval; + current->scheduled = tick + interval; + add_timer_interval(data, current); + } else { + if (!freelist_head) { + freelist_head = current; + } + if (freelist_tail) { + freelist_tail->next = current; + } + freelist_tail = current; + + AtomicSet(¤t->canceled, 1); + } + } + + /* Adjust the delay based on processing time */ + now = sys_get_ticks(); + interval = (now - tick); + if (interval > delay) { + delay = 0; + } else { + delay -= interval; + } + + /* Note that each time a timer is added, this will return + immediately, but we process the timers added all at once. + That's okay, it just means we run through the loop a few + extra times. + */ + sys_sem_wait_timeout(&data->sem, delay); + } +} + +int sys_timer_init(void) +{ + timerData *data = &timer_data; + + if (!AtomicGet(&data->active)) { + RecursiveLock_Init(&data->timermap_lock); + LightSemaphore_Init(&data->sem, 0, ((s16)0x7FFF)); + AtomicSet(&data->active, 1); + + /* Timer threads use a callback into the app, so we can't set a limited stack size here. */ + data->thread = threadCreate(timer_thread, + data, + 32 * 1024, + 0x28, + -1, + false); + if (!data->thread) { + sys_timer_quit(); + return -1; + } + + AtomicSet(&data->nextID, 1); + } + return 0; +} + +void sys_timer_quit(void) +{ + timerData *data = &timer_data; + sysTimer *timer; + timerMap *entry; + + if (AtomicCAS(&data->active, 1, 0)) { /* active? Move to inactive. */ + /* Shutdown the timer thread */ + if (data->thread) { + LightSemaphore_Release(&data->sem, 1); + Result res = threadJoin(data->thread, U64_MAX); + threadFree(data->thread); + data->thread = NULL; + } + + /* Clean up the timer entries */ + while (data->timers) { + timer = data->timers; + data->timers = timer->next; + free(timer); + } + while (data->freelist) { + timer = data->freelist; + data->freelist = timer->next; + free(timer); + } + while (data->timermap) { + entry = data->timermap; + data->timermap = entry->next; + free(entry); + } + } +} + +int sys_add_timer(u32 interval, timer_callback_ptr callback, void *param) +{ + timerData *data = &timer_data; + sysTimer *timer; + timerMap *entry; + + AtomicLock(&data->lock); + if (!AtomicGet(&data->active)) { + if (sys_timer_init() < 0) { + AtomicUnlock(&data->lock); + return 0; + } + } + + timer = data->freelist; + if (timer) { + data->freelist = timer->next; + } + AtomicUnlock(&data->lock); + + if (timer) { + sys_remove_timer(timer->timerID); + } else { + timer = (sysTimer *) malloc(sizeof(*timer)); + if (!timer) { + DEBUGF("sys_add_timer: out of memory\n"); + return 0; + } + } + timer->timerID = AtomicIncrement(&data->nextID); + timer->callback = callback; + timer->param = param; + timer->interval = interval; + timer->scheduled = sys_get_ticks() + interval; + AtomicSet(&timer->canceled, 0); + + entry = (timerMap *) malloc(sizeof(*entry)); + if (!entry) { + free(timer); + DEBUGF("sys_add_timer: out of memory\n"); + return 0; + } + entry->timer = timer; + entry->timerID = timer->timerID; + + RecursiveLock_Lock(&data->timermap_lock); + entry->next = data->timermap; + data->timermap = entry; + RecursiveLock_Unlock(&data->timermap_lock); + + /* Add the timer to the pending list for the timer thread */ + AtomicLock(&data->lock); + timer->next = data->pending; + data->pending = timer; + AtomicUnlock(&data->lock); + + /* Wake up the timer thread if necessary */ + LightSemaphore_Release(&data->sem, 1); + + return entry->timerID; +} + +bool sys_remove_timer(int id) +{ + timerData *data = &timer_data; + timerMap *prev, *entry; + bool canceled = false; + + /* Find the timer */ + RecursiveLock_Lock(&data->timermap_lock); + prev = NULL; + for (entry = data->timermap; entry; prev = entry, entry = entry->next) { + if (entry->timerID == id) { + if (prev) { + prev->next = entry->next; + } else { + data->timermap = entry->next; + } + break; + } + } + RecursiveLock_Unlock(&data->timermap_lock); + + if (entry) { + if (!AtomicGet(&entry->timer->canceled)) { + AtomicSet(&entry->timer->canceled, 1); + canceled = true; + } + free(entry); + } + return canceled; +} + diff --git a/firmware/target/hosted/ctru/lib/sys_timer.h b/firmware/target/hosted/ctru/lib/sys_timer.h new file mode 100644 index 0000000000..cccaab4fce --- /dev/null +++ b/firmware/target/hosted/ctru/lib/sys_timer.h @@ -0,0 +1,42 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __SYSTIMER_H__ +#define __SYSTIMER_H__ + +#include <3ds/svc.h> +#include "sys_thread.h" + +typedef u32 (* timer_callback_ptr) (u32 interval, void *param); + +void sys_ticks_init(void); +void sys_ticks_quit(void); +u32 sys_get_ticks(void); +u64 sys_get_ticks64(void); +void sys_delay(u32 ms); + +int sys_timer_init(void); +void sys_timer_quit(void); +int sys_add_timer(u32 interval, timer_callback_ptr callback, void *param); +bool sys_remove_timer(int id); + +#endif /* #ifndef __SYSTIMER_H__ */ + diff --git a/firmware/target/hosted/ctru/luminance-ctru.c b/firmware/target/hosted/ctru/luminance-ctru.c new file mode 100644 index 0000000000..7d1d0a3310 --- /dev/null +++ b/firmware/target/hosted/ctru/luminance-ctru.c @@ -0,0 +1,133 @@ +/* +* This file is part of Luma3DS +* Copyright (C) 2016-2020 Aurora Wright, TuxSH +* +* This program 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 Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program 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 this program. If not, see . +* +* Additional Terms 7.b and 7.c of GPLv3 apply to this file: +* * Requiring preservation of specified reasonable legal notices or +* author attributions in that material or in the Appropriate Legal +* Notices displayed by works containing it. +* * Prohibiting misrepresentation of the origin of that material, +* or requiring that modified versions of such material be marked in +* reasonable ways as different from the original version. +*/ + +/* This file is taken from Luma3DS project */ + +#include <3ds.h> +#include +#include "luminance-ctru.h" + +// For accessing physmem uncached (and directly) +#define PA_PTR(addr) (void *)((u32)(addr) | 1 << 31) + +#ifndef PA_FROM_VA_PTR +#define PA_FROM_VA_PTR(addr) PA_PTR(svcConvertVAToPA((const void *)(addr), false)) +#endif + +#define REG32(addr) (*(vu32 *)(PA_PTR(addr))) + + +extern bool is_n3ds; + +typedef struct BlPwmData +{ + float coeffs[3][3]; + u8 numLevels; + u8 unk; + u16 luminanceLevels[7]; + u16 brightnessMax; + u16 brightnessMin; +} BlPwmData; + +// Calibration, with (dubious) default values as fallback +static BlPwmData s_blPwmData = { + .coeffs = { + { 0.00111639f, 1.41412f, 0.07178809f }, + { 0.000418169f, 0.66567f, 0.06098654f }, + { 0.00208543f, 1.55639f, 0.0385939f } + }, + .numLevels = 5, + .unk = 0, + .luminanceLevels = { 20, 43, 73, 95, 117, 172, 172 }, + .brightnessMax = 512, + .brightnessMin = 13, +}; + +static inline float getPwmRatio(u32 brightnessMax, u32 pwmCnt) +{ + u32 val = (pwmCnt & 0x10000) ? pwmCnt & 0x3FF : 511; // check pwm enabled flag + return (float)brightnessMax / (val + 1); +} + +// nn's asm has rounding errors (originally at 10^-3) +static inline u32 luminanceToBrightness(u32 luminance, const float coeffs[3], u32 minLuminance, float pwmRatio) +{ + float x = (float)luminance; + float y = coeffs[0]*x*x + coeffs[1]*x + coeffs[2]; + y = (y <= minLuminance ? (float)minLuminance : y) / pwmRatio; + + return (u32)(y + 0.5f); +} + +static inline u32 brightnessToLuminance(u32 brightness, const float coeffs[3], float pwmRatio) +{ + // Find polynomial root of ax^2 + bx + c = y + + float y = (float)brightness * pwmRatio; + float a = coeffs[0]; + float b = coeffs[1]; + float c = coeffs[2] - y; + + float x0 = (-b + sqrtf(b*b - 4.0f*a*c)) / (a + a); + + return (u32)(x0 + 0.5f); +} + +static void readCalibration(void) +{ + static bool calibRead = false; + + if (!calibRead) { + cfguInit(); + calibRead = R_SUCCEEDED(CFG_GetConfigInfoBlk8(sizeof(BlPwmData), 0x50002, &s_blPwmData)); + cfguExit(); + } +} + +u32 getMinLuminancePreset(void) +{ + readCalibration(); + return s_blPwmData.luminanceLevels[0]; +} + +u32 getMaxLuminancePreset(void) +{ + readCalibration(); + return s_blPwmData.luminanceLevels[s_blPwmData.numLevels - 1]; +} + +u32 getCurrentLuminance(bool top) +{ + u32 regbase = top ? 0x10202200 : 0x10202A00; + + readCalibration(); + + const float *coeffs = s_blPwmData.coeffs[top ? (is_n3ds ? 2 : 1) : 0]; + u32 brightness = REG32(regbase + 0x40); + float ratio = getPwmRatio(s_blPwmData.brightnessMax, REG32(regbase + 0x44)); + + return brightnessToLuminance(brightness, coeffs, ratio); +} diff --git a/firmware/target/hosted/ctru/luminance-ctru.h b/firmware/target/hosted/ctru/luminance-ctru.h new file mode 100644 index 0000000000..148935b790 --- /dev/null +++ b/firmware/target/hosted/ctru/luminance-ctru.h @@ -0,0 +1,40 @@ +/* +* This file is part of Luma3DS +* Copyright (C) 2016-2020 Aurora Wright, TuxSH +* +* This program 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 Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program 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 this program. If not, see . +* +* Additional Terms 7.b and 7.c of GPLv3 apply to this file: +* * Requiring preservation of specified reasonable legal notices or +* author attributions in that material or in the Appropriate Legal +* Notices displayed by works containing it. +* * Prohibiting misrepresentation of the origin of that material, +* or requiring that modified versions of such material be marked in +* reasonable ways as different from the original version. +*/ + +#ifndef _LUMINANCE_CTRU_H_ +#define _LUMINANCE_CTRU_H_ + +#include <3ds/types.h> + +extern u32 ctru_min_lum; +extern u32 ctru_max_lum; +extern u32 ctru_luminance; + +u32 getMinLuminancePreset(void); +u32 getMaxLuminancePreset(void); +u32 getCurrentLuminance(bool top); + +#endif /* _LUMINANCE_CTRU_H_ */ diff --git a/firmware/target/hosted/ctru/pcm-ctru.c b/firmware/target/hosted/ctru/pcm-ctru.c new file mode 100644 index 0000000000..d7c9d36458 --- /dev/null +++ b/firmware/target/hosted/ctru/pcm-ctru.c @@ -0,0 +1,359 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2005 by Nick Lanham + * Copyright (C) 2010 by Thomas Martitz + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +#include "autoconf.h" + +#include +#include +#include +#include "config.h" +#include "debug.h" +#include "sound.h" +#include "audiohw.h" +#include "system.h" +#include "panic.h" + +#ifdef HAVE_RECORDING +#include "audiohw.h" +#ifdef HAVE_SPDIF_IN +#include "spdif.h" +#endif +#endif + +#include "pcm.h" +#include "pcm-internal.h" +#include "pcm_sampr.h" +#include "pcm_mixer.h" + +#include <3ds/ndsp/ndsp.h> +#include <3ds/ndsp/channel.h> +#include <3ds/services/dsp.h> +#include <3ds/synchronization.h> +#include <3ds/allocator/linear.h> + +/*#define LOGF_ENABLE*/ +#include "logf.h" + +#ifdef DEBUG +extern bool debug_audio; +#endif + +extern const char *audiodev; + +/* Bytes left in the Rockbox PCM frame buffer. */ +static size_t _pcm_buffer_size = 0; + + +/* Rockbox PCM frame buffer. */ +static const void *_pcm_buffer = NULL; + +/* + 1: PCM thread suspended. + 0: PCM thread running. + These are used by pcm_play_[lock|unlock] or pcm_play_dma_[start|stop|pause]. These need to be + separated because of nested calls for suspending and stopping. +*/ +static volatile int _dsp_enabled = 1; +static volatile int _pcm_shutdown = 0; + + +/* Mutex for PCM thread suspend/unsuspend. */ +static RecursiveLock _pcm_lock_mtx; /* audio device mutex */ +static LightEvent _dsp_callback_event; /* dsp callback synchronization flag */ + +static Thread _pcm_thread; +static int _pcm_thread_id = -1; + +/* DSP wave buffers */ +static ndspWaveBuf _dsp_wave_bufs[3]; +static s16 *_dsp_audio_buffer = NULL; + +static inline bool is_in_audio_thread(int audio_thread_id) +{ + /* The device thread locks the same mutex, but not through the public API. + This check is in case the application, in the audio callback, + tries to lock the thread that we've already locked from the + device thread...just in case we only have non-recursive mutexes. */ + if ( (sys_thread_id() == audio_thread_id)) { + return true; + } + + return false; +} + +void pcm_play_lock(void) +{ + if (!is_in_audio_thread(_pcm_thread_id)) { + RecursiveLock_Lock(&_pcm_lock_mtx); + } +} + +void pcm_play_unlock(void) +{ + if (!is_in_audio_thread(_pcm_thread_id)) { + RecursiveLock_Unlock(&_pcm_lock_mtx); + } +} + +static void pcm_write_to_soundcard(const void *pcm_buffer, size_t pcm_buffer_size, ndspWaveBuf *dsp_buffer) +{ + s16 *buffer = dsp_buffer->data_pcm16; + memcpy(buffer, pcm_buffer, pcm_buffer_size); + + dsp_buffer->nsamples = pcm_buffer_size / 2 / sizeof(s16); + ndspChnWaveBufAdd(0, dsp_buffer); + DSP_FlushDataCache(buffer, pcm_buffer_size); +} + +bool fill_buffer(ndspWaveBuf *dsp_buffer) +{ + if(_pcm_buffer_size == 0) + { + /* Retrive a new PCM buffer from Rockbox. */ + if(!pcm_play_dma_complete_callback(PCM_DMAST_OK, &_pcm_buffer, &_pcm_buffer_size)) + { + /* DEBUGF("DEBUG %s: No new buffer.\n", __func__); */ + svcSleepThread(10000); + return false; + } + } + pcm_play_dma_status_callback(PCM_DMAST_STARTED); + + /* This relies on Rockbox PCM frame buffer size == ALSA PCM frame buffer size. */ + pcm_write_to_soundcard(_pcm_buffer, _pcm_buffer_size, dsp_buffer); + _pcm_buffer_size = 0; + + return true; +} + +void pcm_thread_run(void* nothing) +{ + (void) nothing; + + DEBUGF("DEBUG %s: Thread start.\n", __func__); + + _pcm_thread_id = sys_thread_id(); + + while(!AtomicGet(&_pcm_shutdown)) + { + RecursiveLock_Lock(&_pcm_lock_mtx); + for(size_t i = 0; i < ARRAY_SIZE(_dsp_wave_bufs); ++i) { + if(_dsp_wave_bufs[i].status != NDSP_WBUF_DONE) { + continue; + } + + if(!fill_buffer(&_dsp_wave_bufs[i])) { + continue; + } + } + RecursiveLock_Unlock(&_pcm_lock_mtx); + + // Wait for a signal that we're needed again before continuing, + // so that we can yield to other things that want to run + // (Note that the 3DS uses cooperative threading) + LightEvent_Wait(&_dsp_callback_event); + } + + DEBUGF("DEBUG %s: Thread end.\n", __func__); +} + +void dsp_callback(void *const nul_) { + (void)nul_; + + if(AtomicGet(&_pcm_shutdown)) { + return; + } + + LightEvent_Signal(&_dsp_callback_event); +} + +static void pcm_dma_apply_settings_nolock(void) +{ + ndspChnReset(0); + + ndspSetOutputMode(NDSP_OUTPUT_STEREO); + + ndspChnSetRate(0, pcm_sampr); + ndspChnSetFormat(0, NDSP_FORMAT_STEREO_PCM16); + /* ndspChnSetInterp(0, NDSP_INTERP_POLYPHASE); */ + /* ndspChnSetInterp(0, NDSP_INTERP_NONE); */ + ndspChnSetInterp(0, NDSP_INTERP_LINEAR); + + float mix[12]; + memset(mix, 0, sizeof(mix)); + mix[0] = 1.0; + mix[1] = 1.0; + ndspChnSetMix(0, mix); + + memset(_dsp_wave_bufs, 0, sizeof(_dsp_wave_bufs) * ARRAY_SIZE(_dsp_wave_bufs)); + + const size_t wave_buffer_size = MIX_FRAME_SAMPLES * 2 * sizeof(s16); + size_t buffer_size = wave_buffer_size * ARRAY_SIZE(_dsp_wave_bufs); + + _dsp_audio_buffer = (s16 *)linearAlloc(buffer_size); + + s16 *buffer = _dsp_audio_buffer; + for (unsigned i = 0; i < ARRAY_SIZE(_dsp_wave_bufs); i++) { + _dsp_wave_bufs[i].data_vaddr = buffer; + _dsp_wave_bufs[i].status = NDSP_WBUF_DONE; + + buffer += wave_buffer_size / sizeof(buffer[0]); + } + + ndspChnSetPaused(0, true); + ndspSetCallback(dsp_callback, NULL); + + AtomicSet(&_pcm_shutdown, 0); + AtomicSet(&_dsp_enabled, 1); + + // Start the thread, passing our opusFile as an argument. + _pcm_thread = threadCreate(pcm_thread_run, + NULL, + 32 * 1024, /* 32kB stack size */ + 0x18, /* high priority */ + -1, /* run on any core */ false); +} + +void pcm_play_dma_start(const void *addr, size_t size) +{ + _pcm_buffer = addr; + _pcm_buffer_size = size; + + RecursiveLock_Lock(&_pcm_lock_mtx); + ndspChnSetPaused(0, false); + RecursiveLock_Unlock(&_pcm_lock_mtx); +} + +void pcm_play_dma_stop(void) +{ + RecursiveLock_Lock(&_pcm_lock_mtx); + ndspChnSetPaused(0, true); + RecursiveLock_Unlock(&_pcm_lock_mtx); +} + +/* TODO: implement recording */ +#ifdef HAVE_RECORDING +void pcm_rec_lock(void) +{ +} + +void pcm_rec_unlock(void) +{ +} + +void pcm_rec_dma_init(void) +{ +} + +void pcm_rec_dma_close(void) +{ +} + +void pcm_rec_dma_start(void *start, size_t size) +{ + (void)start; + (void)size; + + +} + +void pcm_rec_dma_stop(void) +{ +} + +const void * pcm_rec_dma_get_peak_buffer(void) +{ + return NULL; +} + +void audiohw_set_recvol(int left, int right, int type) +{ + (void)left; + (void)right; + (void)type; +} + +#ifdef HAVE_SPDIF_IN +unsigned long spdif_measure_frequency(void) +{ + return 0; +} +#endif + +#endif /* HAVE_RECORDING */ + +void pcm_play_dma_init(void) +{ + Result ndsp_init_res = ndspInit(); + if (R_FAILED(ndsp_init_res)) { + if ((R_SUMMARY(ndsp_init_res) == RS_NOTFOUND) && (R_MODULE(ndsp_init_res) == RM_DSP)) { + logf("DSP init failed: dspfirm.cdc missing!"); + } else { + logf("DSP init failed. Error code: 0x%lX", ndsp_init_res); + } + return; + } + + RecursiveLock_Init(&_pcm_lock_mtx); + LightEvent_Init(&_dsp_callback_event, RESET_ONESHOT); +} + +void pcm_play_dma_postinit(void) +{ +} + +void pcm_dma_apply_settings(void) +{ + pcm_play_lock(); + pcm_dma_apply_settings_nolock(); + pcm_play_unlock(); +} + +void pcm_close_device(void) +{ + RecursiveLock_Lock(&_pcm_lock_mtx); + AtomicSet(&_pcm_shutdown, 1); + LightEvent_Signal(&_dsp_callback_event); + + threadJoin(_pcm_thread, UINT64_MAX); + threadFree(_pcm_thread); + _pcm_thread_id = -1; + RecursiveLock_Unlock(&_pcm_lock_mtx); + + ndspSetCallback(NULL, NULL); + + ndspChnReset(0); + if (_dsp_audio_buffer != NULL) { + linearFree(_dsp_audio_buffer); + _dsp_audio_buffer = NULL; + } + + ndspExit(); +} + +/* moved from drivers/audio/ctru.c */ +void audiohw_close(void) +{ + pcm_close_device(); +} diff --git a/firmware/target/hosted/ctru/powermgmt-ctru.c b/firmware/target/hosted/ctru/powermgmt-ctru.c new file mode 100644 index 0000000000..063abf7a7b --- /dev/null +++ b/firmware/target/hosted/ctru/powermgmt-ctru.c @@ -0,0 +1,107 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include + +#include "config.h" +#include "kernel.h" +#include "powermgmt.h" +#include "power.h" +#include "adc.h" +#include "system.h" +#include "debug.h" + +#include <3ds/types.h> +#include <3ds/result.h> +#include <3ds/services/mcuhwc.h> +#include <3ds/services/ptmu.h> + +void mcuhwc_init(void) +{ + Result result = mcuHwcInit(); + if (R_FAILED(result)) { + DEBUGF("mcuhwc_init: warning, mcuHwcInit failed\n"); + } + + result = ptmuInit(); + if (R_FAILED(result)) { + DEBUGF("mcuhwc_init: warning, ptmuInit failed\n"); + } +} + +void mcuhwc_close(void) +{ + mcuHwcExit(); + ptmuExit(); +} + +/* FIXME: what level should disksafe be?*/ +unsigned short battery_level_disksafe = 0; + +unsigned short battery_level_shutoff = 0; + +/* voltages (millivolt) of 0%, 10%, ... 100% when charging disabled */ +unsigned short percent_to_volt_discharge[11] = +{ +}; + +/* voltages (millivolt) of 0%, 10%, ... 100% when charging enabled */ +unsigned short percent_to_volt_charge[11] = +{ + 3450, 3670, 3721, 3751, 3782, 3821, 3876, 3941, 4034, 4125, 4200 +}; + +enum +{ + BATT_NOT_CHARGING = 0, + BATT_CHARGING, +}; + +static u8 charging_status = BATT_NOT_CHARGING; + +unsigned int power_input_status(void) +{ + unsigned status = POWER_INPUT_NONE; + PTMU_GetBatteryChargeState(&charging_status); + if (charging_status == BATT_CHARGING) + status = POWER_INPUT_MAIN_CHARGER; + return status; +} + +/* Returns battery voltage from MAX17040 VCELL ADC [millivolts steps], + * adc returns voltage in 1.25mV steps */ +/* + * TODO this would be interesting to be mixed with battery percentage, for information + * and completition purpouses + */ +int _battery_level(void) +{ + u8 level = 100; + MCUHWC_GetBatteryLevel(&level); + return level; +} + +bool charging_state(void) +{ + return (charging_status == BATT_CHARGING); +} + diff --git a/firmware/target/hosted/ctru/system-ctru.c b/firmware/target/hosted/ctru/system-ctru.c new file mode 100644 index 0000000000..a091e889c6 --- /dev/null +++ b/firmware/target/hosted/ctru/system-ctru.c @@ -0,0 +1,145 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2006 by Daniel Everton + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include +#include +#include +#include +#include "system.h" +#include "kernel.h" +#include "thread-ctru.h" +#include "system-ctru.h" +#include "button-ctru.h" +#include "lcd-bitmap.h" +#include "panic.h" +#include "debug.h" + +#include <3ds/types.h> +#include <3ds/allocator/linear.h> +#include <3ds/services/cfgu.h> +#include "bfile.h" + +const char *audiodev = NULL; + +#ifdef DEBUG +bool debug_audio = false; +#endif + +/* default main thread priority, low */ +s32 main_thread_priority = 0x30; +bool is_n3ds = false; + +/* filesystem */ +sync_RWMutex file_internal_mrsw; +FS_Archive sdmcArchive; + +void ctru_sys_quit(void) +{ + sys_poweroff(); +} + +void power_off(void) +{ + /* since sim_thread_shutdown() grabs the mutex we need to let it free, + * otherwise sys_wait_thread will deadlock */ + struct thread_entry* t = sim_thread_unlock(); + sim_thread_shutdown(); + + /* lock again before entering the scheduler */ + sim_thread_lock(t); + /* sim_thread_shutdown() will cause sim_do_exit() to be called via longjmp, + * but only if we let the sdl thread scheduler exit the other threads */ + while(1) yield(); +} + +void sim_do_exit() +{ + sim_kernel_shutdown(); + sys_timer_quit(); + /* TODO: quit_everything() */ + exit(EXIT_SUCCESS); +} + +uintptr_t *stackbegin; +uintptr_t *stackend; +void system_init(void) +{ + /* fake stack, OS manages size (and growth) */ + volatile uintptr_t stack = 0; + stackbegin = stackend = (uintptr_t*) &stack; + + /* disable sleep mode when lid is closed */ + aptSetSleepAllowed(false); + + sys_console_init(); + sys_timer_init(); + + svcGetThreadPriority(&main_thread_priority, CUR_THREAD_HANDLE); + if (main_thread_priority != 0x30) { + DEBUGF("warning, main_thread_priority = 0x%x\n", main_thread_priority); + } + + /* check for New 3DS model */ + s64 dummyInfo; + is_n3ds = svcGetSystemInfo(&dummyInfo, 0x10001, 0) == 0; + + /* filesystem */ + sync_RWMutexInit(&file_internal_mrsw); + Result res = FSUSER_OpenArchive(&sdmcArchive, + ARCHIVE_SDMC, + fsMakePath(PATH_ASCII, "")); + if (R_FAILED(res)) { + DEBUGF("FSUSER_OpenArchive failed\n"); + exit(-1); + } + + mcuhwc_init(); + cfguInit(); +} + + +void system_reboot(void) +{ + sim_thread_exception_wait(); +} + +void system_exception_wait(void) +{ + system_reboot(); +} + +int hostfs_init(void) +{ + /* stub */ + /* romfsInit(); */ + return 0; +} + +#ifdef HAVE_STORAGE_FLUSH +int hostfs_flush(void) +{ +#ifdef __unix__ + sync(); +#endif + return 0; +} +#endif /* HAVE_STORAGE_FLUSH */ + diff --git a/firmware/target/hosted/ctru/system-ctru.h b/firmware/target/hosted/ctru/system-ctru.h new file mode 100644 index 0000000000..23c63a3c0c --- /dev/null +++ b/firmware/target/hosted/ctru/system-ctru.h @@ -0,0 +1,70 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 by Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef _SYSTEM_SDL_H_ +#define _SYSTEM_SDL_H_ + +#include +#include +#include "config.h" +#include "gcc_extensions.h" + +#include <3ds/types.h> +#include <3ds/svc.h> +#include "sys_timer.h" + +#define HIGHEST_IRQ_LEVEL 1 + +int set_irq_level(int level); + +#define disable_irq() \ + ((void)set_irq_level(HIGHEST_IRQ_LEVEL)) + +#define enable_irq() \ + ((void)set_irq_level(0)) + +#define disable_irq_save() \ + set_irq_level(HIGHEST_IRQ_LEVEL) + +#define restore_irq(level) \ + ((void)set_irq_level(level)) + +#define wait_for_interrupt() + +#include "system-hosted.h" + +void sim_enter_irq_handler(void); +void sim_exit_irq_handler(void); +void sim_kernel_shutdown(void); +void sys_poweroff(void); +void sys_handle_argv(int argc, char *argv[]); +void gui_message_loop(void); +void sim_do_exit(void) NORETURN_ATTR; +void sdl_sys_quit(void); + +void mcuhwc_init(void); +void mcuhwc_close(void); + +extern bool background; /* True if the background image is enabled */ +extern bool showremote; +extern double display_zoom; +extern long start_tick; + +#endif /* _SYSTEM_SDL_H_ */ diff --git a/firmware/target/hosted/ctru/system-target.h b/firmware/target/hosted/ctru/system-target.h new file mode 100644 index 0000000000..79df842405 --- /dev/null +++ b/firmware/target/hosted/ctru/system-target.h @@ -0,0 +1,4 @@ +#include "system-ctru.h" + +#define NEED_GENERIC_BYTESWAPS + diff --git a/firmware/target/hosted/ctru/thread-ctru.c b/firmware/target/hosted/ctru/thread-ctru.c new file mode 100644 index 0000000000..675cb314be --- /dev/null +++ b/firmware/target/hosted/ctru/thread-ctru.c @@ -0,0 +1,503 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2006 Dan Everton + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "autoconf.h" + +#include +#include +#include +#include /* memset() */ +#include +#include "system-ctru.h" +#include "thread-ctru.h" +#include "sys_thread.h" +#include "../kernel-internal.h" +#include "core_alloc.h" + +/* Define this as 1 to show informational messages that are not errors. */ +#define THREAD_DEBUGF_ENABLED 0 + +#if THREAD_DEBUGF_ENABLED +#define THREAD_DEBUGF(...) DEBUGF(__VA_ARGS__) +static char __name[sizeof (((struct thread_debug_info *)0)->name)]; +#define THREAD_GET_NAME(thread) \ + ({ format_thread_name(__name, sizeof (__name), thread); __name; }) +#else +#define THREAD_DEBUGF(...) +#define THREAD_GET_NAME(thread) +#endif + +#define THREAD_PANICF(str...) \ + ({ fprintf(stderr, str); exit(-1); }) + +/* Jump buffers for graceful exit - kernel threads don't stay neatly + * in their start routines responding to messages so this is the only + * way to get them back in there so they may exit */ +static jmp_buf thread_jmpbufs[MAXTHREADS]; +/* this mutex locks out other Rockbox threads while one runs, + * that enables us to simulate a cooperative environment even if + * the host is preemptive */ +static RecursiveLock m; +#define THREADS_RUN 0 +#define THREADS_EXIT 1 +#define THREADS_EXIT_COMMAND_DONE 2 +static volatile int threads_status = THREADS_RUN; + +extern long start_tick; + +void sim_thread_shutdown(void) +{ + int i; + + /* This *has* to be a push operation from a thread not in the pool + so that they may be dislodged from their blocking calls. */ + + /* Tell all threads jump back to their start routines, unlock and exit + gracefully - we'll check each one in turn for it's status. Threads + _could_ terminate via thread_exit or multiple threads could exit + on each unlock but that is safe. */ + + /* Do this before trying to acquire lock */ + threads_status = THREADS_EXIT; + + /* Take control */ + RecursiveLock_Lock(&m); + + /* Signal all threads on delay or block */ + for (i = 0; i < MAXTHREADS; i++) + { + struct thread_entry *thread = __thread_slot_entry(i); + if (thread->context.s == NULL) + continue; + LightSemaphore_Release(thread->context.s, 1); + } + + /* Wait for all threads to finish and cleanup old ones. */ + for (i = 0; i < MAXTHREADS; i++) + { + struct thread_entry *thread = __thread_slot_entry(i); + sysThread *t = thread->context.t; + + if (t != NULL) + { + RecursiveLock_Unlock(&m); + /* Wait for it to finish */ + sys_wait_thread(t, NULL); + /* Relock for next thread signal */ + RecursiveLock_Lock(&m); + /* Already waited and exiting thread would have waited .told, + * replacing it with t. */ + thread->context.told = NULL; + } + else + { + /* Wait on any previous thread in this location-- could be one not + * quite finished exiting but has just unlocked the mutex. If it's + * NULL, the call returns immediately. + * + * See thread_exit below for more information. */ + sys_wait_thread(thread->context.told, NULL); + } + } + + RecursiveLock_Unlock(&m); + + /* Signal completion of operation */ + threads_status = THREADS_EXIT_COMMAND_DONE; +} + +void sim_thread_exception_wait(void) +{ + while (1) + { + sys_delay(HZ/10); + if (threads_status != THREADS_RUN) + thread_exit(); + } +} + +/* A way to yield and leave the threading system for extended periods */ +void sim_thread_lock(void *me) +{ + RecursiveLock_Lock(&m); + __running_self_entry() = (struct thread_entry *)me; + + if (threads_status != THREADS_RUN) + thread_exit(); +} + +void * sim_thread_unlock(void) +{ + struct thread_entry *current = __running_self_entry(); + RecursiveLock_Unlock(&m); + return current; +} + +void switch_thread(void) +{ + struct thread_entry *current = __running_self_entry(); + + enable_irq(); + + switch (current->state) + { + case STATE_RUNNING: + { + RecursiveLock_Unlock(&m); + /* Any other thread waiting already will get it first */ + RecursiveLock_Lock(&m); + break; + } /* STATE_RUNNING: */ + + case STATE_BLOCKED: + { + int oldlevel; + + RecursiveLock_Unlock(&m); + sys_sem_wait(current->context.s); + RecursiveLock_Lock(&m); + + oldlevel = disable_irq_save(); + current->state = STATE_RUNNING; + restore_irq(oldlevel); + break; + } /* STATE_BLOCKED: */ + + case STATE_BLOCKED_W_TMO: + { + int result, oldlevel; + + RecursiveLock_Unlock(&m); + result = sys_sem_wait_timeout(current->context.s, current->tmo_tick); + RecursiveLock_Lock(&m); + + oldlevel = disable_irq_save(); + + current->state = STATE_RUNNING; + + if (result == 1) + { + /* Other signals from an explicit wake could have been made before + * arriving here if we timed out waiting for the semaphore. Make + * sure the count is reset. */ + while (sys_sem_value(current->context.s) > 0) + sys_sem_try_wait(current->context.s); + } + + restore_irq(oldlevel); + break; + } /* STATE_BLOCKED_W_TMO: */ + + case STATE_SLEEPING: + { + RecursiveLock_Unlock(&m); + sys_sem_wait_timeout(current->context.s, current->tmo_tick); + RecursiveLock_Lock(&m); + current->state = STATE_RUNNING; + break; + } /* STATE_SLEEPING: */ + } + +#ifdef BUFLIB_DEBUG_CHECK_VALID + core_check_valid(); +#endif + __running_self_entry() = current; + + if (threads_status != THREADS_RUN) + thread_exit(); +} + +void sleep_thread(int ticks) +{ + struct thread_entry *current = __running_self_entry(); + int rem; + + current->state = STATE_SLEEPING; + + rem = (sys_get_ticks() - start_tick) % (1000/HZ); + if (rem < 0) + rem = 0; + + current->tmo_tick = (1000/HZ) * ticks + ((1000/HZ)-1) - rem; +} + +void block_thread_(struct thread_entry *current, int ticks) +{ + if (ticks < 0) + current->state = STATE_BLOCKED; + else + { + current->state = STATE_BLOCKED_W_TMO; + current->tmo_tick = (1000/HZ)*ticks; + } + + wait_queue_register(current); +} + +unsigned int wakeup_thread_(struct thread_entry *thread + IF_PRIO(, enum wakeup_thread_protocol proto)) +{ + switch (thread->state) + { + case STATE_BLOCKED: + case STATE_BLOCKED_W_TMO: + wait_queue_remove(thread); + thread->state = STATE_RUNNING; + LightSemaphore_Release(thread->context.s, 1); + return THREAD_OK; + } + + return THREAD_NONE; + (void) proto; +} + +void thread_thaw(unsigned int thread_id) +{ + struct thread_entry *thread = __thread_id_entry(thread_id); + + if (thread->id == thread_id && thread->state == STATE_FROZEN) + { + thread->state = STATE_RUNNING; + LightSemaphore_Release(thread->context.s, 1); + } +} + +int runthread(void *data) +{ + /* Cannot access thread variables before locking the mutex as the + data structures may not be filled-in yet. */ + RecursiveLock_Lock(&m); + + struct thread_entry *current = (struct thread_entry *)data; + __running_self_entry() = current; + + jmp_buf *current_jmpbuf = &thread_jmpbufs[THREAD_ID_SLOT(current->id)]; + + /* Setup jump for exit */ + if (setjmp(*current_jmpbuf) == 0) + { + /* Run the thread routine */ + if (current->state == STATE_FROZEN) + { + RecursiveLock_Unlock(&m); + sys_sem_wait(current->context.s); + RecursiveLock_Lock(&m); + __running_self_entry() = current; + } + + if (threads_status == THREADS_RUN) + { + current->context.start(); + THREAD_DEBUGF("Thread Done: %d (%s)\n", + THREAD_ID_SLOT(current->id), + THREAD_GET_NAME(current)); + /* Thread routine returned - suicide */ + } + + thread_exit(); + } + else + { + /* Unlock and exit */ + RecursiveLock_Unlock(&m); + } + + return 0; +} + +unsigned int create_thread(void (*function)(void), + void* stack, size_t stack_size, + unsigned flags, const char *name + IF_PRIO(, int priority) + IF_COP(, unsigned int core)) +{ + THREAD_DEBUGF("Creating thread: (%s)\n", name ? name : ""); + + struct thread_entry *thread = thread_alloc(); + if (thread == NULL) + { + DEBUGF("Failed to find thread slot\n"); + return 0; + } + + LightSemaphore *s = (LightSemaphore *) malloc(sizeof(LightSemaphore)); + if (s == NULL) + { + DEBUGF("Failed to create semaphore\n"); + return 0; + } + + LightSemaphore_Init(s, 0, 255); + + sysThread *t = sys_create_thread(runthread, + name, + stack_size, + thread + IF_PRIO(, priority) + IF_COP(, core)); + if (t == NULL) + { + DEBUGF("Failed to create thread\n"); + free(s); + return 0; + } + + thread->name = name; + thread->state = (flags & CREATE_THREAD_FROZEN) ? + STATE_FROZEN : STATE_RUNNING; + thread->context.start = function; + thread->context.t = t; + thread->context.s = s; + thread->priority = priority; + + THREAD_DEBUGF("New Thread: %lu (%s)\n", + (unsigned long)thread->id, + THREAD_GET_NAME(thread)); + + return thread->id; + (void)stack; +} + +void thread_exit(void) +{ + struct thread_entry *current = __running_self_entry(); + + int oldlevel = disable_irq_save(); + + sysThread *t = current->context.t; + LightSemaphore *s = current->context.s; + + /* Wait the last thread here and keep this one or ctru will leak it since + * it doesn't free its own library allocations unless a wait is performed. + * Such behavior guards against the memory being invalid by the time + * sys_wait_thread is reached and also against two different threads having + * the same pointer. It also makes sys_wait_thread a non-concurrent function. + * + * However, see more below about sys_kill_thread. + */ + sys_wait_thread(current->context.told, NULL); + + current->context.t = NULL; + current->context.s = NULL; + current->context.told = t; + + unsigned int id = current->id; + new_thread_id(current); + current->state = STATE_KILLED; + wait_queue_wake(¤t->queue); + + free(s); + + /* Do a graceful exit - perform the longjmp back into the thread + function to return */ + restore_irq(oldlevel); + + thread_free(current); + + longjmp(thread_jmpbufs[THREAD_ID_SLOT(id)], 1); + + /* This should never and must never be reached - if it is, the + * state is corrupted */ + THREAD_PANICF("thread_exit->K:*R (ID: %d)", id); + while (1); +} + +void thread_wait(unsigned int thread_id) +{ + struct thread_entry *current = __running_self_entry(); + struct thread_entry *thread = __thread_id_entry(thread_id); + + if (thread->id == thread_id && thread->state != STATE_KILLED) + { + block_thread(current, TIMEOUT_BLOCK, &thread->queue, NULL); + switch_thread(); + } +} + +int thread_set_priority(unsigned int thread_id, int priority) +{ + struct thread_entry *thread = __thread_id_entry(thread_id); + sysThread *t = thread->context.t; + thread->priority = sys_set_thread_priority(t, priority); + return thread->priority; +} + +int thread_get_priority(unsigned int thread_id) +{ + struct thread_entry *thread = __thread_id_entry(thread_id); + return thread->priority; +} + +/* Initialize threading */ +void init_threads(void) +{ + RecursiveLock_Init(&m); + RecursiveLock_Lock(&m); + + thread_alloc_init(); + + struct thread_entry *thread = thread_alloc(); + if (thread == NULL) + { + fprintf(stderr, "Main thread alloc failed\n"); + return; + } + + /* Slot 0 is reserved for the main thread - initialize it here and + then create the thread - it is possible to have a quick, early + shutdown try to access the structure. */ + thread->name = __main_thread_name; + thread->state = STATE_RUNNING; + thread->context.s = (LightSemaphore *) malloc(sizeof(LightSemaphore)); + LightSemaphore_Init(thread->context.s, 0, 255); + thread->context.t = NULL; /* NULL for the implicit main thread */ + __running_self_entry() = thread; + + if (thread->context.s == NULL) + { + fprintf(stderr, "Failed to create main semaphore\n"); + return; + } + + /* Tell all threads jump back to their start routines, unlock and exit + gracefully - we'll check each one in turn for it's status. Threads + _could_ terminate via thread_exit or multiple threads could exit + on each unlock but that is safe. */ + + /* Setup jump for exit */ + if (setjmp(thread_jmpbufs[THREAD_ID_SLOT(thread->id)]) == 0) + { + THREAD_DEBUGF("Main Thread: %lu (%s)\n", + (unsigned long)thread->id, + THREAD_GET_NAME(thread)); + return; + } + + RecursiveLock_Unlock(&m); + + /* Set to 'COMMAND_DONE' when other rockbox threads have exited. */ + while (threads_status < THREADS_EXIT_COMMAND_DONE) + sys_delay(10); + + /* We're the main thead - perform exit - doesn't return. */ + sim_do_exit(); +} + diff --git a/firmware/target/hosted/ctru/thread-ctru.h b/firmware/target/hosted/ctru/thread-ctru.h new file mode 100644 index 0000000000..89e2d121cb --- /dev/null +++ b/firmware/target/hosted/ctru/thread-ctru.h @@ -0,0 +1,32 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2025 Mauricio G. + * + * This program 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 Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#ifndef __THREAD_CTRU_H__ +#define __THREAD_CTRU_H__ + +/* extra thread functions that only apply when running on hosting platforms */ +void sim_thread_lock(void *me); +void * sim_thread_unlock(void); +void sim_thread_exception_wait(void); +void sim_thread_shutdown(void); /* Shut down all kernel threads gracefully */ + +#endif /* #ifndef __THREAD_CTRU_H__ */ + diff --git a/firmware/target/hosted/ctru/timer-ctru.c b/firmware/target/hosted/ctru/timer-ctru.c new file mode 100644 index 0000000000..4c608b667b --- /dev/null +++ b/firmware/target/hosted/ctru/timer-ctru.c @@ -0,0 +1,61 @@ +/*************************************************************************** +* __________ __ ___. +* Open \______ \ ____ ____ | | _\_ |__ _______ ___ +* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / +* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < +* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ +* \/ \/ \/ \/ \/ +* $Id$ +* +* Copyright (C) 2025 Mauricio G. +* +* This program 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 Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +* KIND, either express or implied. +* +****************************************************************************/ + +#include "sys_timer.h" +#include "timer.h" + +static int timer_prio = -1; +void (*global_timer_callback)(void); +int timerId; + +u32 _timer_callback(u32 interval, void *param){ + (void)param; + global_timer_callback(); + return(interval); +} + +#define cycles_to_miliseconds(cycles) \ + ((int)((1000*cycles)/TIMER_FREQ)) + +bool timer_register(int reg_prio, void (*unregister_callback)(void), + long cycles, void (*timer_callback)(void)) +{ + (void)unregister_callback; + if (reg_prio <= timer_prio || cycles == 0) + return false; + timer_prio=reg_prio; + global_timer_callback=timer_callback; + timerId=sys_add_timer(cycles_to_miliseconds(cycles), _timer_callback, 0); + return true; +} + +bool timer_set_period(long cycles) +{ + sys_remove_timer(timerId); + timerId=sys_add_timer(cycles_to_miliseconds(cycles), _timer_callback, 0); + return true; +} + +void timer_unregister(void) +{ + sys_remove_timer(timerId); + timer_prio = -1; +} diff --git a/firmware/target/hosted/ctru/timer-ctru_sys.c b/firmware/target/hosted/ctru/timer-ctru_sys.c new file mode 100644 index 0000000000..4c608b667b --- /dev/null +++ b/firmware/target/hosted/ctru/timer-ctru_sys.c @@ -0,0 +1,61 @@ +/*************************************************************************** +* __________ __ ___. +* Open \______ \ ____ ____ | | _\_ |__ _______ ___ +* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / +* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < +* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ +* \/ \/ \/ \/ \/ +* $Id$ +* +* Copyright (C) 2025 Mauricio G. +* +* This program 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 Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +* KIND, either express or implied. +* +****************************************************************************/ + +#include "sys_timer.h" +#include "timer.h" + +static int timer_prio = -1; +void (*global_timer_callback)(void); +int timerId; + +u32 _timer_callback(u32 interval, void *param){ + (void)param; + global_timer_callback(); + return(interval); +} + +#define cycles_to_miliseconds(cycles) \ + ((int)((1000*cycles)/TIMER_FREQ)) + +bool timer_register(int reg_prio, void (*unregister_callback)(void), + long cycles, void (*timer_callback)(void)) +{ + (void)unregister_callback; + if (reg_prio <= timer_prio || cycles == 0) + return false; + timer_prio=reg_prio; + global_timer_callback=timer_callback; + timerId=sys_add_timer(cycles_to_miliseconds(cycles), _timer_callback, 0); + return true; +} + +bool timer_set_period(long cycles) +{ + sys_remove_timer(timerId); + timerId=sys_add_timer(cycles_to_miliseconds(cycles), _timer_callback, 0); + return true; +} + +void timer_unregister(void) +{ + sys_remove_timer(timerId); + timer_prio = -1; +} diff --git a/packaging/ctru/ctru.make b/packaging/ctru/ctru.make new file mode 100644 index 0000000000..288b4ac3bd --- /dev/null +++ b/packaging/ctru/ctru.make @@ -0,0 +1,101 @@ +# __________ __ ___. +# Open \______ \ ____ ____ | | _\_ |__ _______ ___ +# Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / +# Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < +# Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ +# \/ \/ \/ \/ \/ +# $Id$ +# + +# 3ds_rules +export DEVKITARM ?= /opt/devkitpro/devkitARM +export DEVKITPRO ?= /opt/devkitpro + +PORTLIBS := $(DEVKITPRO)/portlibs/3ds + +CTRULIB ?= $(DEVKITPRO)/libctru + +export PATH := $(DEVKITPRO)/portlibs/3ds/bin:$(PATH) + +# base_rules +export SHELL := /usr/bin/env bash + +DEVKITPATH=$(shell echo "$(DEVKITPRO)" | sed -e 's/^\([a-zA-Z]\):/\/\1/') + +export PATH := $(DEVKITPATH)/tools/bin:$(DEVKITPATH)/devkitARM/bin:$(PATH) + +# 3DSX +VERSION_MAJOR := 1 +VERSION_MINOR := 0 +VERSION_MICRO := 0 + +APP_TITLE := rockbox +APP_DESCRIPTION := Open Source Jukebox Firmware +APP_AUTHOR := rockbox.org +APP_ICON := $(ROOTDIR)/packaging/ctru/res/icon.png + +# CIA +BANNER_AUDIO := $(ROOTDIR)/packaging/ctru/res/banner.wav +BANNER_IMAGE := $(ROOTDIR)/packaging/ctru/res/banner.cgfx +RSF_PATH := $(ROOTDIR)/packaging/ctru/res/app.rsf +#LOGO := $(ROOTDIR)/packaging/ctru/logo.lz11 +UNIQUE_ID := 0xCB001 +PRODUCT_CODE := CTR-ROCKBOX +ICON_FLAGS := nosavebackups,visible + +# CIA Configuration +USE_ON_SD := true +APP_ENCRYPTED := false +CATEGORY := Application +USE_ON_SD := true +MEMORY_TYPE := Application +SYSTEM_MODE := 64MB +SYSTEM_MODE_EXT := Legacy +CPU_SPEED := 268MHz +ENABLE_L2_CACHE := false + +.SECONDEXPANSION: # $$(OBJ) is not populated until after this + +MAKEROM ?= makerom +MAKEROM_ARGS := -elf "$(BINARY).elf" -rsf "$(RSF_PATH)" -banner "$(BUILDDIR)/banner.bnr" -icon "$(BUILDDIR)/icon.icn" -DAPP_TITLE="$(APP_TITLE)" -DAPP_PRODUCT_CODE="$(PRODUCT_CODE)" -DAPP_UNIQUE_ID="$(UNIQUE_ID)" -DAPP_ENCRYPTED="$(APP_ENCRYPTED)" -DAPP_SYSTEM_MODE="$(SYSTEM_MODE)" -DAPP_SYSTEM_MODE_EXT="$(SYSTEM_MODE_EXT)" -DAPP_CATEGORY="$(CATEGORY)" -DAPP_USE_ON_SD="$(USE_ON_SD)" -DAPP_MEMORY_TYPE="$(MEMORY_TYPE)" -DAPP_CPU_SPEED="$(CPU_SPEED)" -DAPP_ENABLE_L2_CACHE="$(ENABLE_L2_CACHE)" +MAKEROM_ARGS += -major $(VERSION_MAJOR) -minor $(VERSION_MINOR) -micro $(VERSION_MICRO) + +ifneq ($(strip $(LOGO)),) + MAKEROM_ARGS += -logo "$(LOGO)" +endif +ifneq ($(strip $(ROMFS)),) + MAKEROM_ARGS += -DAPP_ROMFS="$(ROMFS)" +endif + +BANNERTOOL ?= bannertool + +ifeq ($(suffix $(BANNER_IMAGE)),.cgfx) + BANNER_IMAGE_ARG := -ci +else + BANNER_IMAGE_ARG := -i +endif + +ifeq ($(suffix $(BANNER_AUDIO)),.cwav) + BANNER_AUDIO_ARG := -ca +else + BANNER_AUDIO_ARG := -a +endif + +# main binary +$(BUILDDIR)/$(BINARY): $$(OBJ) $(FIRMLIB) $(VOICESPEEXLIB) $(CORE_LIBS) +ifeq ($(UNAME), Darwin) + $(call PRINTS,LD $(BINARY))$(CC) -o $@ $^ $(LDOPTS) $(GLOBAL_LDOPTS) -Wl,$(LDMAP_OPT),$(BUILDDIR)/rockbox.map +else + $(call PRINTS,LD $(BINARY))$(CC) -o $@ -Wl,--start-group $^ -Wl,--end-group $(LDOPTS) $(GLOBAL_LDOPTS) \ + -Wl,$(LDMAP_OPT),$(BUILDDIR)/rockbox-.map + @mv $(BINARY) $(BINARY).elf + smdhtool --create "$(APP_TITLE)" "$(APP_DESCRIPTION)" "$(APP_AUTHOR)" $(APP_ICON) "rockbox.smdh" + 3dsxtool $(BINARY).elf $(BINARY).3dsx --smdh="rockbox.smdh" + $(BANNERTOOL) makebanner $(BANNER_IMAGE_ARG) "$(BANNER_IMAGE)" $(BANNER_AUDIO_ARG) "$(BANNER_AUDIO)" -o "$(BUILDDIR)/banner.bnr" + $(BANNERTOOL) makesmdh -s "$(APP_TITLE)" -l "$(APP_DESCRIPTION)" -p "$(APP_AUTHOR)" -i "$(APP_ICON)" -f "$(ICON_FLAGS)" -o "$(BUILDDIR)/icon.icn" + $(MAKEROM) -f cia -o "$(BINARY).cia" -target t -exefslogo $(MAKEROM_ARGS) +ifndef DEBUG + $(SILENT)rm $(BINARY).elf +endif +endif + diff --git a/packaging/ctru/res/app.rsf b/packaging/ctru/res/app.rsf new file mode 100644 index 0000000000..e89849df48 --- /dev/null +++ b/packaging/ctru/res/app.rsf @@ -0,0 +1,286 @@ +BasicInfo: + Title : $(APP_TITLE) + ProductCode : $(APP_PRODUCT_CODE) + Logo : Homebrew + +RomFs: + RootPath: $(APP_ROMFS) + +TitleInfo: + Category : $(APP_CATEGORY) + UniqueId : $(APP_UNIQUE_ID) + +Option: + UseOnSD : $(APP_USE_ON_SD) # true if App is to be installed to SD + FreeProductCode : true # Removes limitations on ProductCode + MediaFootPadding : false # If true CCI files are created with padding + EnableCrypt : $(APP_ENCRYPTED) # Enables encryption for NCCH and CIA + EnableCompress : true # Compresses where applicable (currently only exefs:/.code) + +AccessControlInfo: + CoreVersion : 2 + + # Exheader Format Version + DescVersion : 2 + + # Minimum Required Kernel Version (below is for 4.5.0) + ReleaseKernelMajor : "02" + ReleaseKernelMinor : "33" + + # ExtData + UseExtSaveData : false # enables ExtData + #ExtSaveDataId : 0x300 # only set this when the ID is different to the UniqueId + + # FS:USER Archive Access Permissions + # Uncomment as required + FileSystemAccess: + - CategorySystemApplication + - CategoryHardwareCheck + - CategoryFileSystemTool + - Debug + - TwlCardBackup + - TwlNandData + - Boss + - DirectSdmc + - Core + - CtrNandRo + - CtrNandRw + - CtrNandRoWrite + - CategorySystemSettings + - CardBoard + - ExportImportIvs + - DirectSdmcWrite + - SwitchCleanup + - SaveDataMove + - Shop + - Shell + - CategoryHomeMenu + - SeedDB + IoAccessControl: + - FsMountNand + - FsMountNandRoWrite + - FsMountTwln + - FsMountWnand + - FsMountCardSpi + - UseSdif3 + - CreateSeed + - UseCardSpi + + # Process Settings + MemoryType : $(APP_MEMORY_TYPE) # Application/System/Base + SystemMode : $(APP_SYSTEM_MODE) # 64MB(Default)/96MB/80MB/72MB/32MB + IdealProcessor : 0 + AffinityMask : 1 + Priority : 16 + MaxCpu : 0x9E # Default + HandleTableSize : 0x200 + DisableDebug : false + EnableForceDebug : false + CanWriteSharedPage : true + CanUsePrivilegedPriority : false + CanUseNonAlphabetAndNumber : true + PermitMainFunctionArgument : true + CanShareDeviceMemory : true + RunnableOnSleep : false + SpecialMemoryArrange : true + + # New3DS Exclusive Process Settings + SystemModeExt : $(APP_SYSTEM_MODE_EXT) # Legacy(Default)/124MB/178MB Legacy:Use Old3DS SystemMode + CpuSpeed : $(APP_CPU_SPEED) # 268MHz(Default)/804MHz + EnableL2Cache : $(APP_ENABLE_L2_CACHE) # false(default)/true + CanAccessCore2 : true + + # Virtual Address Mappings + IORegisterMapping: + - 1ff00000-1ff7ffff # DSP memory + MemoryMapping: + - 1f000000-1f5fffff:r # VRAM + + # Accessible SVCs, : + SystemCallAccess: + ControlMemory: 1 + QueryMemory: 2 + ExitProcess: 3 + GetProcessAffinityMask: 4 + SetProcessAffinityMask: 5 + GetProcessIdealProcessor: 6 + SetProcessIdealProcessor: 7 + CreateThread: 8 + ExitThread: 9 + SleepThread: 10 + GetThreadPriority: 11 + SetThreadPriority: 12 + GetThreadAffinityMask: 13 + SetThreadAffinityMask: 14 + GetThreadIdealProcessor: 15 + SetThreadIdealProcessor: 16 + GetCurrentProcessorNumber: 17 + Run: 18 + CreateMutex: 19 + ReleaseMutex: 20 + CreateSemaphore: 21 + ReleaseSemaphore: 22 + CreateEvent: 23 + SignalEvent: 24 + ClearEvent: 25 + CreateTimer: 26 + SetTimer: 27 + CancelTimer: 28 + ClearTimer: 29 + CreateMemoryBlock: 30 + MapMemoryBlock: 31 + UnmapMemoryBlock: 32 + CreateAddressArbiter: 33 + ArbitrateAddress: 34 + CloseHandle: 35 + WaitSynchronization1: 36 + WaitSynchronizationN: 37 + SignalAndWait: 38 + DuplicateHandle: 39 + GetSystemTick: 40 + GetHandleInfo: 41 + GetSystemInfo: 42 + GetProcessInfo: 43 + GetThreadInfo: 44 + ConnectToPort: 45 + SendSyncRequest1: 46 + SendSyncRequest2: 47 + SendSyncRequest3: 48 + SendSyncRequest4: 49 + SendSyncRequest: 50 + OpenProcess: 51 + OpenThread: 52 + GetProcessId: 53 + GetProcessIdOfThread: 54 + GetThreadId: 55 + GetResourceLimit: 56 + GetResourceLimitLimitValues: 57 + GetResourceLimitCurrentValues: 58 + GetThreadContext: 59 + Break: 60 + OutputDebugString: 61 + ControlPerformanceCounter: 62 + CreatePort: 71 + CreateSessionToPort: 72 + CreateSession: 73 + AcceptSession: 74 + ReplyAndReceive1: 75 + ReplyAndReceive2: 76 + ReplyAndReceive3: 77 + ReplyAndReceive4: 78 + ReplyAndReceive: 79 + BindInterrupt: 80 + UnbindInterrupt: 81 + InvalidateProcessDataCache: 82 + StoreProcessDataCache: 83 + FlushProcessDataCache: 84 + StartInterProcessDma: 85 + StopDma: 86 + GetDmaState: 87 + RestartDma: 88 + DebugActiveProcess: 96 + BreakDebugProcess: 97 + TerminateDebugProcess: 98 + GetProcessDebugEvent: 99 + ContinueDebugEvent: 100 + GetProcessList: 101 + GetThreadList: 102 + GetDebugThreadContext: 103 + SetDebugThreadContext: 104 + QueryDebugProcessMemory: 105 + ReadProcessMemory: 106 + WriteProcessMemory: 107 + SetHardwareBreakPoint: 108 + GetDebugThreadParam: 109 + ControlProcessMemory: 112 + MapProcessMemory: 113 + UnmapProcessMemory: 114 + CreateCodeSet: 115 + CreateProcess: 117 + TerminateProcess: 118 + SetProcessResourceLimits: 119 + CreateResourceLimit: 120 + SetResourceLimitValues: 121 + AddCodeSegment: 122 + Backdoor: 123 + KernelSetState: 124 + QueryProcessMemory: 125 + + # Service List + # Maximum 34 services (32 if firmware is prior to 9.6.0) + ServiceAccessControl: + - APT:U + - ac:u + - am:net + - boss:U + - cam:u + - cecd:u + - cfg:nor + - cfg:u + - csnd:SND + - dsp::DSP + - frd:u + - fs:USER + - gsp::Gpu + - gsp::Lcd + - hid:USER + - http:C + - ir:rst + - ir:u + - ir:USER + - mic:u + - mcu::HWC + - ndm:u + - news:s + - nwm::EXT + - nwm::UDS + - ptm:sysm + - ptm:u + - pxi:dev + - soc:U + - ssl:C + - y2r:u + + +SystemControlInfo: + SaveDataSize: 0KB # Change if the app uses savedata + RemasterVersion: $(APP_VERSION_MAJOR) + StackSize: 0x40000 + + # Modules that run services listed above should be included below + # Maximum 48 dependencies + # : + Dependency: + ac: 0x0004013000002402 + #act: 0x0004013000003802 + am: 0x0004013000001502 + boss: 0x0004013000003402 + camera: 0x0004013000001602 + cecd: 0x0004013000002602 + cfg: 0x0004013000001702 + codec: 0x0004013000001802 + csnd: 0x0004013000002702 + dlp: 0x0004013000002802 + dsp: 0x0004013000001a02 + friends: 0x0004013000003202 + gpio: 0x0004013000001b02 + gsp: 0x0004013000001c02 + hid: 0x0004013000001d02 + http: 0x0004013000002902 + i2c: 0x0004013000001e02 + ir: 0x0004013000003302 + mcu: 0x0004013000001f02 + mic: 0x0004013000002002 + ndm: 0x0004013000002b02 + news: 0x0004013000003502 + #nfc: 0x0004013000004002 + nim: 0x0004013000002c02 + nwm: 0x0004013000002d02 + pdn: 0x0004013000002102 + ps: 0x0004013000003102 + ptm: 0x0004013000002202 + #qtm: 0x0004013020004202 + ro: 0x0004013000003702 + socket: 0x0004013000002e02 + spi: 0x0004013000002302 + ssl: 0x0004013000002f02 diff --git a/packaging/ctru/res/banner.cgfx b/packaging/ctru/res/banner.cgfx new file mode 100644 index 0000000000..e806a310bb Binary files /dev/null and b/packaging/ctru/res/banner.cgfx differ diff --git a/packaging/ctru/res/banner.png b/packaging/ctru/res/banner.png new file mode 100644 index 0000000000..87c0ae0f66 Binary files /dev/null and b/packaging/ctru/res/banner.png differ diff --git a/packaging/ctru/res/banner.wav b/packaging/ctru/res/banner.wav new file mode 100644 index 0000000000..90d7b2c9ca Binary files /dev/null and b/packaging/ctru/res/banner.wav differ diff --git a/packaging/ctru/res/banner_.cgfx b/packaging/ctru/res/banner_.cgfx new file mode 100644 index 0000000000..028c466d3f Binary files /dev/null and b/packaging/ctru/res/banner_.cgfx differ diff --git a/packaging/ctru/res/icon.png b/packaging/ctru/res/icon.png new file mode 100644 index 0000000000..d788de7205 Binary files /dev/null and b/packaging/ctru/res/icon.png differ diff --git a/packaging/ctru/res/icon_yellow.png b/packaging/ctru/res/icon_yellow.png new file mode 100644 index 0000000000..76c2f42b7f Binary files /dev/null and b/packaging/ctru/res/icon_yellow.png differ diff --git a/packaging/ctru/rockbox.xml b/packaging/ctru/rockbox.xml new file mode 100644 index 0000000000..0e01ecc6e7 --- /dev/null +++ b/packaging/ctru/rockbox.xml @@ -0,0 +1,7 @@ + + /3ds/.rockbox/rockbox.3dsx + .rockbox/icon.icn + rockbox + Open Source Jukebox Firmware + rockbox.org + \ No newline at end of file