
Vuoi creare il tuo gioco homebrew per Nintendo 3DS? Buona notizia: si può ancora fare, ed è un progetto davvero divertente. In questo tutorial ti racconto tutto quello che ho imparato sviluppando 2048 per Nintendo 3DS, un gioco homebrew completo con animazioni, audio, 13 lingue e un sistema di achievement, dalla prima riga di codice alla generazione di un file .cia installabile.
Guarda il risultato finale: 2048 per Nintendo 3DS, scaricabile gratuitamente in .3dsx e .cia.
Questa guida copre l’intera pipeline dello sviluppo homebrew 3DS: grafica 2D con citro2d, audio NDSP, touch screen, file system RomFS e packaging in CIA. Tutto il codice è in C99. Niente teoria a vuoto: solo codice che gira, preso direttamente dal progetto 2048-3DS.
Sommario
- Che cos’è un homebrew 3DS
- Prerequisiti: installare devkitARM e gli strumenti di sviluppo 3DS
- Architettura di un progetto homebrew: separare logica e rendering
- Gestire il doppio schermo del Nintendo 3DS
- Rendering grafico 2D con citro2d
- Gestione degli input: D-pad, circle pad e touch screen
- Audio homebrew 3DS: NDSP e formato WAV PCM
- File system: RomFS e scheda SD
- Salvataggio dei dati su scheda SD
- Il Makefile 3DS: cross-compilazione ARM con devkitARM
- Generare un file .3dsx per l’Homebrew Launcher
- Costruire un file .cia installabile per il menu HOME
- Trappole ed errori frequenti nello sviluppo homebrew 3DS
- Conclusione e risorse per approfondire
1. Che cos’è un homebrew per Nintendo 3DS?
Un homebrew 3DS è un’applicazione non ufficiale che gira su un Nintendo 3DS con custom firmware (CFW), per esempio Luma3DS. La scena homebrew 3DS permette di creare e distribuire liberamente giochi, emulatori e utility per questa console portatile.
Il Nintendo 3DS monta un processore ARM11 (ARMv6K) a 268 MHz e una GPU PICA200, con una particolarità unica: due schermi. Quello superiore è da 400×240 pixel (800×240 in 3D stereoscopico), quello inferiore, touch, da 320×240 pixel.
Per gli homebrew 3DS esistono due formati di distribuzione:
- .3dsx: il formato homebrew che si avvia dall’Homebrew Launcher. Nessuna installazione, esecuzione diretta dalla scheda SD.
- .cia: il formato installabile che compare nel menu HOME del 3DS, con icona e banner animato. Richiede un custom firmware (CFW).
Per 2048-3DS genero entrambi i formati, così da raggiungere più giocatori possibile. La community homebrew 3DS è ancora ben viva grazie a un ecosistema di strumenti solidi: devkitARM (toolchain), libctru (libreria di sistema), citro2d/citro3d (grafica). Se cerchi un buon terreno di gioco per imparare la programmazione di sistemi embedded o lo sviluppo di giochi retro, il 3DS è perfetto.
2. Prerequisiti: installare devkitARM e gli strumenti di sviluppo 3DS
Prima di iniziare a programmare un gioco per Nintendo 3DS devi installare la toolchain di cross-compilazione ARM e le librerie specifiche della console.
Toolchain devkitARM: il compilatore per Nintendo 3DS
devkitARM è la toolchain GCC di cross-compilazione per processori ARM, distribuita dal progetto devkitPro. Include il compilatore, il linker e gli strumenti di build necessari per compilare codice C per il 3DS. Ecco come installarla:
# Installazione su Linux/WSL (consigliata per lo sviluppo 3DS)
wget https://apt.devkitpro.org/install-devkitpro-pacman
chmod +x install-devkitpro-pacman
sudo ./install-devkitpro-pacman
# Installa i pacchetti di sviluppo 3DS
sudo dkp-pacman -S 3ds-dev
# Variabile d'ambiente obbligatoria
export DEVKITARM=/opt/devkitpro/devkitARMLibrerie essenziali per lo sviluppo homebrew 3DS
- libctru: libreria C di basso livello per i servizi di sistema del 3DS (HID, filesystem, audio, GPU). È la base di ogni programma homebrew 3DS.
- citro3d: astrazione sopra la GPU PICA200. Gestisce i render target, il framebuffer e la sincronizzazione grafica.
- citro2d: strato 2D costruito sopra citro3d. Offre primitive semplici (rettangoli, cerchi, ellissi, linee, testo, sprite) ideali per i giochi 2D su 3DS.
Strumenti aggiuntivi per il packaging
- bannertool: genera i file banner.bin e icon.bin richiesti dal formato CIA
- makerom: assembla il file .cia finale a partire da ELF, RSF, banner e icona
- tex3ds: converte immagini PNG in texture .t3x leggibili da citro2d sulla GPU PICA200
- mkbcfnt: converte font TTF nel formato .bcfnt per il rendering del testo su 3DS (supporta Unicode, CJK, cirillico)
3. Architettura di un progetto homebrew 3DS: separare logica e rendering
L’errore classico di chi inizia con lo sviluppo homebrew è scrivere tutto il codice direttamente sulle API della console. Risultato: impossibile testare o fare debug come si deve. Ci sono passato anch’io, e il consiglio che darei a chiunque è separare la logica di gioco dal rendering grafico fin dall’inizio.
È esattamente la strategia usata per 2048-3DS: la logica del gioco 2048 (griglia, mosse, fusioni, punteggio) è portabile al 100%, mentre il rendering è specifico di ogni piattaforma.
Struttura dei file del progetto 2048-3DS
2048-3ds/
├── source/
│ ├── logic.h # Interface du jeu 2048 (portable, sans dépendance)
│ ├── logic.c # Logique du jeu (grille 4x4, mouvements, score)
│ ├── achievements.h # Système de succès (8 paliers de score)
│ ├── achievements.c # Sauvegarde/chargement binaire des succès
│ ├── lang.h # Localisation (13 langues, compile-time)
│ ├── main_sdl.c # Rendu PC avec SDL2 (simulation double écran)
│ └── main_3ds.c # Rendu Nintendo 3DS avec citro2d
├── romfs/ # Assets embarqués dans le binaire .3dsx/.cia
│ ├── font.bcfnt # Police custom avec glyphes CJK/cyrillique
│ ├── sprites.t3x # Sprite sheet compile (icônes de succès)
│ └── music/ # Musique et effets sonores (WAV PCM obligatoire)
├── assets/ # Ressources PC + assets pour bannière CIA
├── gfx/ # Sources des sprites (PNG + config .t3s)
├── Makefile # Build PC
├── Makefile.3ds # Build 3DS (devkitARM + citro2d)
└── app.rsf # Configuration CIA (permissions, titre, UniqueId)Codice portabile: la logica del gioco 2048 senza dipendenze
Il file logic.c di 2048-3DS non include nessun header specifico del Nintendo 3DS. Usa solo header C standard (<stdint.h>, <stdlib.h>, <string.h>, <time.h>). Il file main_3ds.c chiama le funzioni pubbliche della logica:
// logic.h — interfaccia portabile del gioco 2048
#define GRID_SIZE 4
#define MAX_TILE_ANIMS 16
typedef enum { DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT } Direction;
typedef struct {
int from_r, from_c; // posizione prima della mossa
int to_r, to_c; // posizione dopo la mossa
uint16_t value; // valore mostrato durante l'animazione
int merged; // 1 se fusione
} TileAnim;
typedef struct {
uint16_t cells[GRID_SIZE][GRID_SIZE]; // griglia 4x4
uint32_t score;
int won, over;
TileAnim anims[MAX_TILE_ANIMS]; // dati di animazione dell'ultima mossa
int anim_count;
int spawn_r, spawn_c; // posizione della nuova tessera
uint16_t spawn_val; // valore (2 al 90%, 4 al 10%)
} Game;
void game_init(Game *g); // griglia vuota + 2 tessere
int game_move(Game *g, Direction dir); // restituisce 1 se la griglia è cambiata
int game_is_over(Game *g); // nessuna mossa possibile
int game_has_won(Game *g); // tessera >= 2048 raggiuntaIl Makefile 3DS compila logic.c + achievements.c + main_3ds.c filtrando esplicitamente main_sdl.c. La logica di gioco resta identica qualunque sia il front-end di rendering.
4. Gestire il doppio schermo del Nintendo 3DS
La caratteristica che distingue il Nintendo 3DS da ogni altra console portatile è il doppio schermo. Sfruttarlo bene è essenziale per una buona esperienza homebrew.
- Schermo superiore (top): 400×240 pixel, non touch. Ideale per il gameplay principale. In 2048-3DS è qui che mostro la griglia 4×4 con le animazioni delle tessere.
- Schermo inferiore (bottom): 320×240 pixel, touch. Ideale per l’interfaccia, i menu e i controlli. In 2048-3DS ci metto punteggio, pulsanti, impostazioni e achievement.
Inizializzare i render target con citro2d
// Inizializzazione del sistema grafico 3DS
gfxInitDefault();
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
C2D_Prepare();
// Crea un render target per ogni schermo
C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);Loop di rendering su due schermi
A ogni frame devi disegnare sui due schermi separatamente. Ecco la struttura del loop di rendering usata in 2048-3DS:
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
// Schermo superiore — griglia del gioco 2048
C2D_TargetClear(top, couleur_fond);
C2D_SceneBegin(top);
render_top_screen(&game, anim_phase, progress);
// Schermo inferiore — UI (punteggio, pulsanti, impostazioni)
C2D_TargetClear(bot, col_bot_bg);
C2D_SceneBegin(bot);
render_bot_game(&game, best_score, 0, music_muted);
C3D_FrameEnd(0);Trappola critica del rendering 3DS: C2D_TargetClear è obbligatoria. Se non chiami
C2D_TargetClear()prima di ogni scena ottieni artefatti visivi casuali, residui della VRAM del frame precedente. La VRAM del 3DS non viene inizializzata automaticamente. È un bug che disorienta parecchio chi inizia con l’homebrew, perché gli artefatti cambiano a ogni frame.
Dimensioni degli schermi: le costanti essenziali
#define TOP_W 400 // larghezza schermo superiore
#define TOP_H 240 // altezza schermo superiore
#define BOT_W 320 // larghezza schermo inferiore (touch)
#define BOT_H 240 // altezza schermo inferioreAttenzione: sulla console fisica lo schermo inferiore è centrato orizzontalmente rispetto a quello superiore (400 contro 320 pixel di larghezza). Le coordinate touch corrispondono direttamente ai pixel dello schermo inferiore, con (0,0) in alto a sinistra.
5. Rendering grafico 2D con citro2d su Nintendo 3DS
citro2d è la libreria di rendering 2D di riferimento per i giochi homebrew 3DS. Costruita sopra citro3d e la GPU PICA200, gestisce internamente il batching delle primitive e le texture GPU. Ecco come si usa, con esempi concreti presi da 2048-3DS.
Disegnare primitive: rettangoli, cerchi, ellissi e linee
// Rettangolo pieno (tessere del gioco 2048)
C2D_DrawRectSolid(x, y, z, largeur, hauteur, couleur);
// Cerchio pieno (angoli arrotondati, icone)
C2D_DrawCircleSolid(centre_x, centre_y, z, rayon, couleur);
// Ellisse piena (icona della musica in 2048-3DS)
C2D_DrawEllipseSolid(x, y, z, largeur, hauteur, couleur);
// Linea (barra del mute audio)
C2D_DrawLine(x1, y1, couleur1, x2, y2, couleur2, epaisseur, z);Il parametro z controlla la profondità (0.0f per il piano standard). Sul 3DS i colori usano il formato ABGR in memoria (attenzione: è l’inverso del classico ARGB). citro2d mette a disposizione la macro C2D_Color32(r, g, b, a) per costruire i colori nel modo giusto:
// Trappola C99: C2D_Color32() non è constexpr
// Usa una macro per le costanti di colore globali
#define MAKE_COLOR(r,g,b,a) \
((u32)(r) | ((u32)(g)<<8) | ((u32)(b)<<16) | ((u32)(a)<<24))
// Palette di colori del gioco 2048 su 3DS
#define col_grid_bg MAKE_COLOR(0xBB, 0xAD, 0xA0, 0xFF) // sfondo della griglia
#define col_bot_bg MAKE_COLOR(0xFA, 0xF8, 0xEF, 0xFF) // sfondo schermo inferiore
#define col_text_dk MAKE_COLOR(0x77, 0x6E, 0x65, 0xFF) // testo scuro
#define col_text_lt MAKE_COLOR(0xF9, 0xF6, 0xF2, 0xFF) // testo chiaroRettangoli arrotondati: un trucco grafico per citro2d
citro2d non offre una primitiva “rettangolo arrotondato”. Per le tessere del gioco 2048 e i pulsanti dell’interfaccia ho messo a punto questa tecnica, che combina un rettangolo centrale, due rettangoli laterali e quattro cerchi agli angoli:
static void fill_rounded_rect(float x, float y, float w, float h,
float r, u32 clr)
{
if (r < 1.0f || w < 2*r || h < 2*r) {
C2D_DrawRectSolid(x, y, 0.0f, w, h, clr);
return;
}
// Corpo centrale + bordi laterali
C2D_DrawRectSolid(x + r, y, 0.0f, w - 2*r, h, clr);
C2D_DrawRectSolid(x, y + r, 0.0f, r, h - 2*r, clr);
C2D_DrawRectSolid(x + w - r, y + r, 0.0f, r, h - 2*r, clr);
// 4 cerchi di raggio r agli angoli
C2D_DrawCircleSolid(x + r, y + r, 0.0f, r, clr);
C2D_DrawCircleSolid(x + w - r, y + r, 0.0f, r, clr);
C2D_DrawCircleSolid(x + r, y + h - r, 0.0f, r, clr);
C2D_DrawCircleSolid(x + w - r, y + h - r, 0.0f, r, clr);
}Questa funzione è usata ovunque in 2048-3DS: tessere di gioco, riquadri del punteggio, pulsanti, slider del volume, finestre di conferma.
Disegnare testo su Nintendo 3DS con il formato .bcfnt
Il rendering del testo su 3DS passa dal formato .bcfnt (Binary CTR Font). Ogni stringa va prima “parsata” in un buffer di testo e poi disegnata. In 2048-3DS questa tecnica serve a mostrare i punteggi, le etichette dei pulsanti in 13 lingue e le notifiche degli achievement:
// Inizializzazione dei buffer di testo
C2D_TextBuf s_dynamicBuf = C2D_TextBufNew(512);
C2D_Font s_font = C2D_FontLoad("romfs:/font.bcfnt");
// Funzione di utilità: disegna testo centrato
static void draw_text_centered(const char *str, float cx, float cy,
float scale, u32 color)
{
C2D_Text text;
C2D_TextFontParse(&text, s_font, s_dynamicBuf, str);
C2D_TextOptimize(&text);
float w, h;
C2D_TextGetDimensions(&text, scale, scale, &w, &h);
C2D_DrawText(&text, C2D_WithColor,
cx - w / 2, cy - h * 0.62f,
0.0f, scale, scale, color);
}
// Importante: svuota il buffer di testo a ogni frame
C2D_TextBufClear(s_dynamicBuf);Il fattore 0.62f è tarato per ottenere un centraggio verticale visivamente corretto del testo.
Sprite e texture GPU in formato .t3x
Le immagini vanno convertite nel formato .t3x (texture 3DS ottimizzata per la GPU PICA200) con lo strumento tex3ds. Un file .t3s descrive lo sprite sheet. In 2048-3DS le 8 icone degli achievement (32×32 pixel ciascuna) stanno in un unico sprite sheet:
# gfx/sprites.t3s — configuration du sprite sheet pour tex3ds
--atlas -f rgba8888 -z auto
sprite_0.png # Debutant (vert)
sprite_1.png # Apprenti (bleu)
sprite_2.png # Competent (violet)
sprite_3.png # Expert (orange)
sprite_4.png # Maitre (rouge)
sprite_5.png # Grand Maitre (rose)
sprite_6.png # Legende (or)
sprite_7.png # Titan (or brillant)// Caricamento dello sprite sheet da RomFS
C2D_SpriteSheet sheet = C2D_SpriteSheetLoad("romfs:/sprites.t3x");
// Disegno di un'icona achievement
C2D_Image img = C2D_SpriteSheetGetImage(sheet, index);
C2D_DrawImageAt(img, x, y, 0.0f, NULL, scale_x, scale_y);
// Disegno in scala di grigi (achievement bloccato)
C2D_ImageTint tint;
C2D_PlainImageTint(&tint, C2D_Color32(128, 128, 128, 255), 1.0f);
C2D_DrawImageAt(img, x, y, 0.0f, &tint, scale_x, scale_y);Animazioni fluide su Nintendo 3DS
Per animazioni fluide in un gioco homebrew 3DS usa osGetTime() (tempo in millisecondi) e l’interpolazione con easing. Ecco il sistema di animazione implementato in 2048-3DS per lo scorrimento e la comparsa delle tessere:
// Timing delle animazioni del gioco 2048
#define ANIM_SLIDE_MS 120 // durata dello scorrimento della tessera
#define ANIM_POP_MS 100 // durata del "pop" (fusione/comparsa)
typedef enum { ANIM_NONE, ANIM_SLIDING, ANIM_POPPING } AnimPhase;
// Easing quadratico: decelerazione naturale
static float ease_out_quad(float t) {
return t * (2.0f - t);
}
// Nel loop principale: interpolazione delle posizioni
u64 now = osGetTime();
u64 elapsed = now - anim_start;
float progress = (float)elapsed / ANIM_SLIDE_MS;
float t = ease_out_quad(progress);
// Scorrimento lineare della tessera
float cur_x = from_px + (to_px - from_px) * t;
float cur_y = from_py + (to_py - from_py) * t;L’animazione si svolge in due fasi: prima lo scorrimento (120 ms), poi il “pop”, le tessere fuse crescono fino al 125% e tornano al 100%, mentre la nuova tessera compare passando da 0 a 100%. Questo sistema rende il gioco 2048 su 3DS soddisfacente quanto la versione originale.
6. Gestione degli input su Nintendo 3DS: D-pad, circle pad e touch screen
Il Nintendo 3DS offre diverse periferiche di input per i giochi homebrew, tutte accessibili tramite la libreria libctru (HID). In 2048-3DS le uso tutte e tre: D-pad e circle pad per spostare le tessere, touch screen per pulsanti e slider.
Leggere i pulsanti fisici del D-pad e i tasti A/B/X/Y
hidScanInput(); // Legge lo stato degli input (1 volta per frame)
u32 kDown = hidKeysDown(); // Pulsanti premuti in questo frame
// D-pad: sposta le tessere nel gioco 2048
if (kDown & KEY_DUP) { dir = DIR_UP; do_move = 1; }
if (kDown & KEY_DDOWN) { dir = DIR_DOWN; do_move = 1; }
if (kDown & KEY_DLEFT) { dir = DIR_LEFT; do_move = 1; }
if (kDown & KEY_DRIGHT) { dir = DIR_RIGHT; do_move = 1; }
// Pulsanti di sistema
if (kDown & KEY_B) { /* navigazione indietro */ }
if (kDown & KEY_START) { break; /* esce dall'applicazione */ }
if (kDown & KEY_SELECT) { game_init(&game); /* nuova partita */ }Circle Pad: lo stick analogico del 3DS con deadzone
circlePosition cpad;
hidCircleRead(&cpad);
// Deadzone di 80 per evitare i falsi positivi
// Intervallo del circle pad: circa da -155 a +155
if (cpad.dy > 80) { dir = DIR_UP; do_move = 1; }
if (cpad.dy < -80) { dir = DIR_DOWN; do_move = 1; }
if (cpad.dx < -80) { dir = DIR_LEFT; do_move = 1; }
if (cpad.dx > 80) { dir = DIR_RIGHT; do_move = 1; }La deadzone bisogna per il circle pad del 3DS. Senza soglia lo stick registra micro-movimenti in continuazione e fa partire mosse indesiderate. Un valore di 80 (su un intervallo di circa -155 / +155) funziona bene nella pratica per un gioco come 2048.
Touch screen: gestire i tocchi sui pulsanti dell’interfaccia
Il touch screen del 3DS è resistivo (a pressione, non capacitivo). Le coordinate sono direttamente in pixel dello schermo inferiore (320×240). In 2048-3DS il touch gestisce i pulsanti “Nuova partita”, “Achievement”, “Impostazioni”, gli slider del volume e la scelta della lingua:
if (kDown & KEY_TOUCH) {
touchPosition touch;
hidTouchRead(&touch);
int mx = touch.px; // 0..319
int my = touch.py; // 0..239
// Rilevamento del tocco sul pulsante "Nuova partita"
float btn_x = (BOT_W - BTN_W) / 2; // centrato orizzontalmente
float btn_y = 112;
if (mx >= btn_x && mx <= btn_x + BTN_W &&
my >= btn_y && my <= btn_y + BTN_H) {
game_init(&game);
anim_phase = ANIM_NONE;
}
}Usa hidKeysDown() con KEY_TOUCH per i tocchi singoli (pulsanti) e hidKeysHeld() per le interazioni continue (slider del volume, trascinamento).
Loop principale di un gioco homebrew 3DS
while (aptMainLoop()) {
u64 now = osGetTime();
audio_music_tick(music_volume, music_muted);
hidScanInput();
u32 kDown = hidKeysDown();
if (kDown & KEY_START) break; // Uscita pulita
// Gestione degli input (D-pad, circle pad, touch)
// Aggiornamento della logica del gioco 2048
// Rendering dei due schermi
// ...
}aptMainLoop() gestisce il ciclo di vita dell’applicazione homebrew: sospensione, chiusura da parte del sistema, ritorno al menu HOME.
7. Audio homebrew 3DS: NDSP e formato WAV PCM
Credimi sulla parola: l’audio su Nintendo 3DS è la parte in cui ho perso più tempo nello sviluppo homebrew. Sulla carta sembra tutto semplice, ma le trappole sono ovunque. Il backend audio del 3DS è NDSP (Nintendo DSP): richiede il firmware del DSP (dspfirm.bin sulla scheda SD) e supporta il mixing multicanale con interpolazione lineare.
In 2048-3DS ho implementato la musica di sottofondo su 4 tracce e gli effetti sonori per le mosse delle tessere e per gli achievement.
Formato audio per il 3DS: WAV PCM obbligatorio
Trappola principale dell’audio 3DS: la console supporta solo il formato WAV PCM grezzo (niente MP3, niente OGG, niente WAV compresso ADPCM). I file devono essere PCM a 8 o 16 bit, mono o stereo. Qualsiasi altro formato viene ignorato in silenzio o provoca un crash.
Allocazione di memoria per l’audio 3DS: linearAlloc al posto di malloc
I buffer audio sul 3DS vanno allocati con linearAlloc(), non con malloc(). La memoria lineare è accessibile direttamente dal processore DSP, l’heap standard no.
// Struttura per memorizzare un file audio WAV su 3DS
typedef struct {
u8 *data; // Dati PCM (allocati con linearAlloc)
u32 size; // Dimensione in byte
u32 sample_rate; // Frequenza di campionamento
u16 channels; // 1 (mono) o 2 (stereo)
u16 bits_per_sample; // 8 o 16 bit
ndspWaveBuf wave_buf; // Buffer NDSP
int loaded; // Flag di caricamento riuscito
} WavSound;Caricare file WAV nell’homebrew 3DS
static int wav_load(WavSound *snd, const char *path)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
// Legge e valida l'header RIFF/WAVE
char riff[4]; u32 file_size; char wave[4];
fread(riff, 1, 4, f);
fread(&file_size, 4, 1, f);
fread(wave, 1, 4, f);
if (memcmp(riff, "RIFF", 4) != 0 ||
memcmp(wave, "WAVE", 4) != 0) {
fclose(f); return -1;
}
// Scorre i chunk WAV (fmt + data)
while (!got_data) {
char chunk_id[4]; u32 chunk_size;
fread(chunk_id, 1, 4, f);
fread(&chunk_size, 4, 1, f);
if (memcmp(chunk_id, "fmt ", 4) == 0) {
// Estrae channels, sample_rate, bits_per_sample
// ...
} else if (memcmp(chunk_id, "data", 4) == 0) {
// IMPORTANTE: linearAlloc, non malloc!
snd->data = (u8 *)linearAlloc(chunk_size);
fread(snd->data, 1, chunk_size, f);
} else {
fseek(f, chunk_size, SEEK_CUR);
}
}
// Prepara il buffer NDSP
snd->wave_buf.data_vaddr = snd->data;
snd->wave_buf.nsamples = snd->size /
(snd->channels * snd->bits_per_sample / 8);
snd->wave_buf.looping = false;
// OBBLIGATORIO: svuota la cache CPU verso il DSP
DSP_FlushDataCache(snd->data, snd->size);
return 0;
}DSP_FlushDataCache è obbligatoria dopo ogni scrittura in un buffer audio del 3DS. Senza questa chiamata il DSP legge dati corrotti, perché la cache della CPU non è coerente con la memoria del DSP. È la causa numero uno dei bug audio silenziosi su Nintendo 3DS.
Inizializzazione audio NDSP
Ecco l’inizializzazione audio implementata in 2048-3DS:
static void audio_init(void)
{
if (ndspInit() != 0) return;
ndspSetOutputMode(NDSP_OUTPUT_STEREO);
// Canale 0: musica di sottofondo
// Canale 1: SFX (movimento della tessera)
// Canale 2: SFX (achievement sbloccato)
for (int ch = 0; ch < 3; ch++) {
ndspChnReset(ch);
ndspChnSetInterp(ch, NDSP_INTERP_LINEAR);
ndspChnSetFormat(ch, NDSP_FORMAT_STEREO_PCM16);
}
}Riprodurre un suono e gestire il volume sul 3DS
// NDSP: aggiunge il buffer audio al canale
snd->wave_buf.status = NDSP_WBUF_FREE; // reinizializza lo status
DSP_FlushDataCache(snd->data, snd->size);
ndspChnWaveBufAdd(channel, &snd->wave_buf);
// Volume NDSP: array di 12 float (mix stereo per canale)
float vol = (float)music_volume / 128.0f; // 0..128 -> 0.0..1.0
float mix[12] = {0};
mix[0] = vol; // canale sinistro
mix[1] = vol; // canale destro
ndspChnSetMix(0, mix);Concatenare automaticamente le tracce musicali
In 2048-3DS quattro tracce musicali si susseguono automaticamente (intro, music1, music2, music3). A ogni frame controllo se la traccia in corso è finita:
static void audio_music_tick(int music_volume, int music_muted)
{
if (!s_audio_init || music_muted) return;
if (!ndspChnIsPlaying(0)) {
// Passa alla traccia successiva (loop ciclico)
s_music_current = (s_music_current + 1) % MUSIC_TRACK_COUNT;
audio_play_current_track(music_volume, music_muted);
}
}Pulizia dell’audio alla chiusura del programma
// Libera correttamente le risorse audio
ndspChnReset(0);
ndspChnReset(1);
ndspChnReset(2);
ndspExit();
// Libera la memoria lineare (non free, ma linearFree!)
for (int i = 0; i < MUSIC_TRACK_COUNT; i++)
linearFree(s_music[i].data);
linearFree(s_sfx_push.data);
linearFree(s_sfx_ach.data);8. File system del 3DS: RomFS e scheda SD
Il Nintendo 3DS mette a disposizione delle applicazioni homebrew due file system: RomFS per gli asset in sola lettura e SDMC per lettura e scrittura sulla scheda SD.
RomFS: incorporare gli asset nel binario homebrew
RomFS (Read-Only Memory FileSystem) permette di incorporare file direttamente nel binario .3dsx o .cia. È perfetto per gli asset che non cambiano: font, texture, musica, sprite. In 2048-3DS la cartella romfs/ contiene il font .bcfnt con supporto CJK, lo sprite sheet degli achievement e 6 file audio WAV:
// Inizializzazione obbligatoria prima di ogni accesso a RomFS
romfsInit();
// Accesso agli asset con il prefisso romfs:/
C2D_Font font = C2D_FontLoad("romfs:/font.bcfnt");
C2D_SpriteSheet sheet = C2D_SpriteSheetLoad("romfs:/sprites.t3x");
wav_load(&s_music[0], "romfs:/music/intro.wav");Il contenuto della cartella romfs/ viene incorporato automaticamente dal Makefile 3DS:
# In Makefile.3ds
ROMFS := romfsFormati di asset specifici del Nintendo 3DS
- .bcfnt: font bitmap compilato con
mkbcfnt. Supporta Unicode completo (CJK, cirillico, lettere accentate). In 2048-3DS un solo font copre tutte le 13 lingue. - .t3x: texture GPU compilata con
tex3ds. Formato ottimizzato per la PICA200. Si carica direttamente in VRAM senza conversione. - .wav: audio PCM grezzo. Nessuna conversione a runtime, ma deve essere PCM non compresso (vedi la sezione sull’audio).
SDMC: salvare dati sulla scheda SD del 3DS
Per il salvataggio dei dati (punteggi, impostazioni, progressi) gli homebrew 3DS scrivono sulla scheda SD usando il prefisso sdmc:/:
// Percorsi di salvataggio per 2048-3DS
#define SAVE_DIR "sdmc:/3ds/2048/"
#define ACH_SAVE_PATH "sdmc:/3ds/2048/achievements.dat"
#define SETTINGS_SAVE_PATH "sdmc:/3ds/2048/settings.dat"
// Crea la cartella di salvataggio all'avvio
#include <sys/stat.h>
mkdir("sdmc:/3ds", 0777);
mkdir(SAVE_DIR, 0777);La scrittura usa le funzioni C standard (fopen, fwrite, fclose). Per le operazioni su file in SDMC non serve nessuna API specifica del 3DS.
9. Salvataggio dei dati su scheda SD in un homebrew 3DS
Per un gioco homebrew 3DS il salvataggio binario è il metodo più diretto ed economico. Ecco come 2048-3DS rende persistenti achievement e impostazioni.
Salvataggio binario del sistema di achievement
Il gioco 2048-3DS ha 8 achievement (soglie di punteggio: 500, 1000, 2500, 5000, 10000, 20000, 50000, 100000 punti). Il salvataggio occupa 8 byte, uno per achievement:
// Salvataggio: 8 byte (0x00 = bloccato, 0x01 = sbloccato)
int achievements_save(const Achievements *a, const char *path)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
for (int i = 0; i < ACH_COUNT; i++) {
uint8_t flag = (uint8_t)a->list[i].unlocked;
fwrite(&flag, 1, 1, f);
}
fclose(f);
return 0;
}
// Caricamento con valori di default se il file non esiste
int achievements_load(Achievements *a, const char *path)
{
achievements_init(a); // tutti bloccati di default
FILE *f = fopen(path, "rb");
if (!f) return -1; // prima esecuzione: nessun file
for (int i = 0; i < ACH_COUNT; i++) {
uint8_t flag = 0;
if (fread(&flag, 1, 1, f) != 1) break;
a->list[i].unlocked = flag ? 1 : 0;
}
fclose(f);
return 0;
}Salvataggio delle impostazioni utente
Le impostazioni di 2048-3DS (lingua, volume della musica, volume degli effetti, stato del mute) sono salvate in 4 byte:
// Formato: [0] = lingua, [1] = volume musica, [2] = volume SFX, [3] = mute
static void settings_save(int music_volume, int sfx_volume, int music_muted)
{
FILE *f = fopen(SETTINGS_SAVE_PATH, "wb");
if (!f) return;
u8 data[4];
data[0] = (u8)lang_current; // enum Language (0..12)
data[1] = (u8)music_volume; // 0..128
data[2] = (u8)sfx_volume; // 0..128
data[3] = (u8)(music_muted ? 1 : 0);
fwrite(data, 1, 4, f);
fclose(f);
}I vantaggi di questo approccio per un gioco homebrew: dimensione fissa, nessun parsing, nessuna dipendenza da una libreria JSON/XML, lettura istantanea. Le impostazioni vengono salvate automaticamente quando l’utente esce dal menu delle impostazioni o dall’applicazione.
10. Il Makefile 3DS: cross-compilazione ARM con devkitARM
Il sistema di build degli homebrew 3DS si appoggia alle regole fornite da devkitARM. Capire il Makefile 3DS è essenziale per fare debug dei problemi di compilazione.
Struttura del Makefile per un progetto homebrew 3DS
# Verifica che devkitARM sia configurato
ifeq ($(strip $(DEVKITARM)),)
$(error "Please set DEVKITARM in your environment")
endif
include $(DEVKITARM)/3ds_rules
# Configurazione del progetto homebrew
TARGET := 2048
BUILD := build_3ds
SOURCES := source
ROMFS := romfs
# Metadati mostrati nell'Homebrew Launcher e nel menu HOME
APP_TITLE := 2048
APP_DESCRIPTION := 2048 puzzle game for 3DS
APP_AUTHOR := GekkodeFlag di compilazione ARM per il Nintendo 3DS
ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft
CFLAGS := -g -Wall -Wextra -O2 -mword-relocations \
-ffunction-sections $(ARCH) -std=c99
CFLAGS += $(INCLUDE) -D__3DS__Cosa fa ogni flag della cross-compilazione ARM per 3DS:
-march=armv6k: architettura del processore ARM11 del Nintendo 3DS-mtune=mpcore: ottimizza il codice per il core MPCore-mfloat-abi=hard: usa la VFP (virgola mobile hardware), cruciale per le prestazioni grafiche-mword-relocations: genera le rilocazioni richieste dal formato 3DSX-ffunction-sections: permette al linker di eliminare il codice morto e ridurre la dimensione del binario-D__3DS__: definisce la macro per gli#ifdefcondizionali tra PC e 3DS
Linking delle librerie citro2d e libctru
LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lcitro2d -lcitro3d -lctru -lmL’ordine delle librerie conta per il linker GNU: -lcitro2d dipende da -lcitro3d, che dipende da -lctru. Vanno sempre elencate dall’astrazione più alta alla più bassa.
Filtrare i sorgenti: escludere il rendering PC dalla build 3DS
# Esclude main_sdl.c dalla build 3DS (si compila solo main_3ds.c)
CFILES := $(filter-out main_sdl.c, \
$(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))))Questo filtro garantisce che nella build 3DS venga compilato solo main_3ds.c.
Comandi di build
# Compila l'homebrew in .3dsx
make -f Makefile.3ds
# Compila e genera un .cia installabile
make -f Makefile.3ds cia
# Pulizia completa
make -f Makefile.3ds clean11. Generare un file .3dsx per l’Homebrew Launcher
Il formato .3dsx è il formato homebrew standard del Nintendo 3DS, avviato dall’Homebrew Launcher. È il più semplice da distribuire: un solo file da copiare sulla scheda SD.
Il Makefile 3DS genera automaticamente due file:
2048.3dsx: l’eseguibile homebrew, con il RomFS incorporato2048.smdh: i metadati SMDH (titolo, autore, descrizione, icona)
SMDH: i metadati del tuo homebrew 3DS
Il file SMDH (Simple Metadata Header) contiene le informazioni mostrate nell’Homebrew Launcher:
# Definiti nel Makefile.3ds
APP_TITLE := 2048
APP_DESCRIPTION := 2048 puzzle game for 3DS
APP_AUTHOR := GekkodeL’icona è un PNG 48×48 pixel, rilevato automaticamente se icon.png o 2048.png esiste nella radice del progetto.
Incorporare il RomFS nel .3dsx
Il RomFS viene integrato direttamente nel file .3dsx. L’utente finale ha un solo file da gestire:
_3DSXFLAGS += --romfs=$(CURDIR)/romfs12. Costruire un file .cia installabile per il menu HOME del 3DS
Il formato .cia (CTR Importable Archive) è il formato installabile del Nintendo 3DS. Una volta installato con FBI o un altro gestore di titoli, il gioco compare nel menu HOME della console con icona personalizzata, banner animato e suono di avvio.
È il formato usato per distribuire 2048 per Nintendo 3DS in versione .cia.
Attenzione: è di gran lunga la fase più delicata dello sviluppo homebrew 3DS. Ho passato un tempo assurdo a capire perché un file .cia potesse funzionare alla perfezione nell’emulatore Citra e crashare subito su un 3DS vero. La causa è quasi sempre un file RSF configurato male. Quello che segue è il risultato di molte ore di test su hardware reale: descrivo ogni parametro così non devi sbatterci la testa anche tu.
Asset necessari per costruire un .cia
- Icona: PNG di 48×48 pixel esatti (mostrata nel menu HOME)
- Immagine del banner: PNG di 256×128 pixel esatti (mostrata in alto quando il gioco è selezionato)
- Suono del banner: WAV PCM 16 bit, 44100 Hz, stereo, circa 3 secondi (riprodotto quando il gioco è selezionato nel menu HOME). Il formato è rigido: usa
ffmpegper convertire:
# Converte qualsiasi audio in WAV compatibile con bannertool
ffmpeg -i music.mp3 -acodec pcm_s16le -ar 44100 -ac 2 -t 3 banner.wavLe fasi di costruzione di un file CIA per il 3DS
La costruzione di un .cia avviene in 4 fasi. L’approccio consigliato è usare un template RSF con variabili $(VARIABLE) sostituite dai flag -DVARIABLE="valeur" di makerom. Così la configurazione (nel Makefile) resta separata dal template dei permessi (nell’RSF), e la build diventa più pulita e riutilizzabile:
# 1. Costruisce il binario ELF
make -f Makefile.3ds
# 2. Genera il banner (immagine + audio di avvio)
bannertool makebanner \
-i assets/banner.png \
-a assets/music/banner.wav \
-o build_3ds/banner.bnr
# 3. Genera l'icona SMDH per il menu HOME
bannertool makesmdh \
-s "2048" \
-l "2048 - puzzle game for 3DS" \
-p "Gekkode" \
-i icon.png \
-o build_3ds/icon.icn
# 4. Assembla il .cia finale con makerom
makerom -f cia -o 2048.cia \
-elf 2048.elf \
-rsf app.rsf \
-target t \
-exefslogo \
-icon build_3ds/icon.icn \
-banner build_3ds/banner.bnr \
-major 1 -minor 0 -micro 0 \
-DAPP_TITLE="2048" \
-DAPP_PRODUCT_CODE="CTR-H-2048" \
-DAPP_UNIQUE_ID="0xF2048" \
-DAPP_ENCRYPTED=false \
-DAPP_SYSTEM_MODE="64MB" \
-DAPP_SYSTEM_MODE_EXT="Legacy" \
-DAPP_CATEGORY="Application" \
-DAPP_USE_ON_SD="true" \
-DAPP_MEMORY_TYPE="Application" \
-DAPP_CPU_SPEED="268MHz" \
-DAPP_ENABLE_L2_CACHE="false" \
-DAPP_VERSION_MAJOR="1" \
-DAPP_ROMFS="romfs"Ogni flag -DXXX="valeur" sostituisce la variabile $(XXX) corrispondente nel file RSF. Questo meccanismo di sostituzione ti permette di avere un unico template RSF riutilizzabile per tutti i tuoi progetti: da un progetto all’altro cambiano solo i flag -D.
I flag makerom importanti:
-target t: target “test” (per gli homebrew non firmati da Nintendo)-exefslogo: include il logo nell’ExeFS (necessario per la splash screen di avvio)-major / -minor / -micro: versione del titolo (mostrata nelle impostazioni di sistema)
Il file RSF: configurare i permessi del tuo homebrew CIA
Il file RSF (ROM Specification File) è il file più critico della build CIA. Definisce i metadati, i permessi di sistema, i servizi autorizzati e le chiamate di sistema disponibili. Un RSF incompleto equivale a un crash immediato su hardware reale, anche se in Citra fila tutto liscio.
Ecco il template RSF completo usato per 2048-3DS, messo a punto dopo parecchi test su hardware reale. Ogni sezione è spiegata subito dopo:
BasicInfo: identità del titolo
BasicInfo:
Title : $(APP_TITLE)
ProductCode : $(APP_PRODUCT_CODE)
Logo : NintendoTitle: il nome mostrato nelle impostazioni di sistema. Usa la variabile$(APP_TITLE)sostituita da-DAPP_TITLE="2048".ProductCode: un identificatore nel formatoCTR-H-XXXX. LaHsta per homebrew. Scegli un codice univoco (es.CTR-H-2048).Logo: la splash screen animata mostrata all’avvio.Nintendo: il logo animato ufficiale 3DS (consigliato, è il comportamento standard)Homebrew: il logo della community homebrewLicensed/Distributed: varianti del logo NintendoNone: da evitare, può provocare crash su alcune versioni del firmware
RomFs: risorse incorporate
RomFs:
RootPath: $(APP_ROMFS)Percorso della cartella romfs/ che contiene gli asset incorporati (sprite, font, audio). Passato con -DAPP_ROMFS="romfs". Se la tua applicazione non ha un RomFS puoi omettere questa sezione.
TitleInfo: identificazione univoca
TitleInfo:
Category : $(APP_CATEGORY)
UniqueId : $(APP_UNIQUE_ID)Category: sempreApplicationper un gioco o una utility homebrew.UniqueId: identificatore esadecimale univoco nell’intervallo0xF0000–0xFFFFF(intervallo riservato agli homebrew). Ogni homebrew installato sulla console deve avere un UniqueId diverso per evitare conflitti. Per 2048 uso0xF2048.
Option: opzioni di packaging
Option:
UseOnSD : $(APP_USE_ON_SD)
FreeProductCode : true
MediaFootPadding : false
EnableCrypt : $(APP_ENCRYPTED)
EnableCompress : trueUseOnSD:true(obbligatorio per un homebrew installato su scheda SD).FreeProductCode:trueperché makerom accetti un ProductCode libero (senza verifica del formato Nintendo).EnableCrypt:false(gli homebrew non hanno le chiavi di cifratura Nintendo).EnableCompress:trueper comprimere la sezione .code dell’ExeFS (riduce la dimensione del .cia).
AccessControlInfo: la sezione critica
È qui che si gioca il 90% dei crash CIA. Questa sezione definisce tutto ciò che la tua applicazione può fare sulla console. Se manca anche una sola chiamata di sistema o un solo servizio, il CIA crasha all’istante su hardware reale, mentre Citra ignora queste restrizioni.
AccessControlInfo:
CoreVersion : 2
DescVersion : 2
ReleaseKernelMajor : "02"
ReleaseKernelMinor : "33"
UseExtSaveData : false
MemoryType : $(APP_MEMORY_TYPE)
SystemMode : $(APP_SYSTEM_MODE)
SystemModeExt : $(APP_SYSTEM_MODE_EXT)
CpuSpeed : $(APP_CPU_SPEED)
EnableL2Cache : $(APP_ENABLE_L2_CACHE)
IdealProcessor : 0
AffinityMask : 1
Priority : 16
MaxCpu : 0x9E
HandleTableSize : 0x200
DisableDebug : false
EnableForceDebug : false
CanWriteSharedPage : true
CanUsePrivilegedPriority : false
CanUseNonAlphabetAndNumber : true
PermitMainFunctionArgument : true
CanShareDeviceMemory : true
RunnableOnSleep : false
SpecialMemoryArrange : true
CanAccessCore2 : trueI parametri da adattare al tuo progetto:
MemoryType:Applicationper un gioco o una utility standard.Systemper un modulo di sistema.SystemMode: quantità di RAM allocata.64MBè lo standard. Usa96MBsolo per le applicazioni esclusive New 3DS che hanno bisogno di più memoria.SystemModeExt:Legacyper la compatibilità Old 3DS + New 3DS.CpuSpeed:268MHzattiva il boost di frequenza su New 3DS (su Old 3DS resta a 268 MHz, è gestito automaticamente).EnableL2Cache:falsedi default. Mettilo atrueper le applicazioni CPU-intensive su New 3DS (può causare instabilità su Old 3DS).
Seguono le quattro sottosezioni dei permessi. Il mio consiglio: includi il set completo. Un homebrew di base non usa tutti questi SVC e servizi, ma dichiararne troppi non pesa né sulle prestazioni né sulla sicurezza di una console con CFW, dimenticarne uno solo, invece, provoca un crash immediato:
FileSystemAccess: accesso al file system
FileSystemAccess:
- CategorySystemApplication
- CategoryHardwareCheck
- CategoryFileSystemTool
- Debug
- TwlCardBackup
- TwlNandData
- Boss
- DirectSdmc
- Core
- CtrNandRo
- CtrNandRw
- CtrNandRoWrite
- CategorySystemSettings
- CardBoard
- ExportImportIvs
- DirectSdmcWrite
- SwitchCleanup
- SaveDataMove
- Shop
- Shell
- CategoryHomeMenu
- SeedDBI più importanti per un homebrew di base: DirectSdmc + DirectSdmcWrite (lettura/scrittura SD) e Core. Ma includere il set completo è più sicuro.
IoAccessControl: accesso I/O di basso livello
IoAccessControl:
- FsMountNand
- FsMountNandRoWrite
- FsMountTwln
- FsMountWnand
- FsMountCardSpi
- UseSdif3
- CreateSeed
- UseCardSpiSystemCallAccess: chiamate di sistema ARM11 (SVC)
Questa lista definisce quali chiamate di sistema (supervisor call) può usare la tua applicazione. È la sezione più critica: se manca un SVC e libctru lo invoca, la console crasha all’istante. Includi tutti gli SVC da 1 a 125:
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 : 125Importante: i numeri non sono continui, ci sono dei buchi (63-70, 89-95, 110-111, 116). È normale: sono SVC riservati o non implementati dal kernel del 3DS.
ServiceAccessControl: servizi di sistema
I servizi sono le API di alto livello del 3DS. Ogni libreria di libctru usa uno o più servizi. Se un servizio non è dichiarato qui, la chiamata a srvGetServiceHandle() fallisce e l’init della libreria corrispondente crasha:
ServiceAccessControl:
- APT:U # Ciclo di vita dell'applicazione (aptMainLoop)
- ac:u # Configurazione di rete
- am:net # Application Manager (installazione dei titoli)
- boss:U # SpotPass
- cam:u # Fotocamera
- cecd:u # StreetPass
- cfg:nor # Configurazione NOR
- cfg:u # Configurazione di sistema (lingua, regione)
- csnd:SND # Audio CSND
- dsp::DSP # Audio NDSP (backend principale)
- frd:u # Lista amici
- fs:USER # File system (SDMC, RomFS)
- gsp::Gpu # GPU PICA200 (grafica)
- gsp::Lcd # Controllo dello schermo LCD
- hid:USER # Input (pulsanti, pad, touch)
- http:C # Client HTTP
- ir:rst # Infrarossi (C-stick New 3DS)
- ir:u # Infrarossi generico
- ir:USER # Infrarossi utente
- mic:u # Microfono
- mcu::HWC # Microcontrollore hardware
- ndm:u # Network Daemon Manager
- news:s # Notifiche
- nwm::EXT # Network Manager esteso
- nwm::UDS # Network Manager UDS (wireless locale)
- ptm:sysm # Power/Timer Manager di sistema
- ptm:u # Power/Timer Manager utente
- pxi:dev # Accesso ai device PXI
- soc:U # Socket (rete TCP/UDP)
- ssl:C # SSL/TLS
- y2r:u # Conversione da YUV a RGBPer un gioco homebrew di base i servizi indispensabili sono: APT:U, fs:USER, gsp::Gpu, hid:USER. Aggiungi dsp::DSP se usi l’audio e cfg:u se leggi la configurazione di sistema. Ma come per gli SVC è più sicuro includere il set completo: dichiarare un servizio non utilizzato non costa nulla.
SystemControlInfo: stack, salvataggio e dipendenze
SystemControlInfo:
SaveDataSize: 0KB
RemasterVersion: $(APP_VERSION_MAJOR)
StackSize: 0x40000
Dependency:
ac: 0x0004013000002402
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
nim: 0x0004013000002c02
nwm: 0x0004013000002d02
pdn: 0x0004013000002102
ps: 0x0004013000003102
ptm: 0x0004013000002202
ro: 0x0004013000003702
socket: 0x0004013000002e02
spi: 0x0004013000002302
ssl: 0x0004013000002f02SaveDataSize:0KBse salvi direttamente sulla SD (come fa la maggior parte degli homebrew). Indica una dimensione se usi l’API di salvataggio di sistema.StackSize:0x40000(256 KB) è un default solido. Aumentalo se la tua app fa ricorsione pesante o alloca array grossi sullo stack.Dependency: i title ID dei moduli di sistema da cui dipende la tua applicazione. Attenzione: i title ID non devono avere il suffissoL. Scrivere0x0004013000002402Linvece di0x0004013000002402può causare errori silenziosi. Riprendi la lista qui sopra così com’è.
IORegisterMapping e MemoryMapping
IORegisterMapping:
- 1ff00000-1ff7ffff
MemoryMapping:
- 1f000000-1f5fffff:rQueste sezioni definiscono gli intervalli di memoria e i registri I/O accessibili. Riprendi i valori qui sopra senza modifiche: coprono le esigenze di tutti gli homebrew standard.
Adattare l’RSF al tuo progetto
Il template RSF qui sopra è generico e riutilizzabile. Per adattarlo a un nuovo progetto basta cambiare i flag -D nel comando makerom:
Flag -D | Valore 2048 | Da adattare |
|---|---|---|
APP_TITLE | 2048 | Nome della tua app |
APP_PRODUCT_CODE | CTR-H-2048 | CTR-H-XXXX univoco |
APP_UNIQUE_ID | 0xF2048 | Hex univoco tra 0xF0000-0xFFFFF |
APP_ROMFS | romfs | Percorso della tua cartella romfs |
APP_SYSTEM_MODE | 64MB | 96MB se esclusivo New 3DS |
APP_VERSION_MAJOR | 1 | Numero di versione major |
Gli altri valori (APP_ENCRYPTED=false, APP_CATEGORY=Application, APP_USE_ON_SD=true e così via) sono gli stessi per tutti gli homebrew.
Automatizzare la build del CIA nel Makefile
# Variabili in cima al Makefile.3ds
APP_TITLE := 2048
APP_PRODUCT_CODE := CTR-H-2048
APP_UNIQUE_ID := 0xF2048
BANNER_IMAGE := $(TOPDIR)/assets/banner.png
BANNER_AUDIO := $(TOPDIR)/assets/music/banner.wav
RSF_FILE := $(TOPDIR)/app.rsf
# Target CIA — un solo "make -f Makefile.3ds cia"
cia: all
@bannertool makebanner -i $(BANNER_IMAGE) -a $(BANNER_AUDIO) \
-o $(BUILD)/banner.bnr
@bannertool makesmdh -s "$(APP_TITLE)" -l "$(APP_TITLE) - $(APP_DESCRIPTION)" \
-p "$(APP_AUTHOR)" -i $(APP_ICON) -o $(BUILD)/icon.icn
@makerom -f cia -o $(TARGET).cia \
-elf $(TARGET).elf \
-rsf $(RSF_FILE) \
-target t \
-exefslogo \
-icon $(BUILD)/icon.icn \
-banner $(BUILD)/banner.bnr \
-major 1 -minor 0 -micro 0 \
-DAPP_TITLE="$(APP_TITLE)" \
-DAPP_PRODUCT_CODE="$(APP_PRODUCT_CODE)" \
-DAPP_UNIQUE_ID="$(APP_UNIQUE_ID)" \
-DAPP_ENCRYPTED=false \
-DAPP_SYSTEM_MODE="64MB" \
-DAPP_SYSTEM_MODE_EXT="Legacy" \
-DAPP_CATEGORY="Application" \
-DAPP_USE_ON_SD="true" \
-DAPP_MEMORY_TYPE="Application" \
-DAPP_CPU_SPEED="268MHz" \
-DAPP_ENABLE_L2_CACHE="false" \
-DAPP_VERSION_MAJOR="1" \
-DAPP_ROMFS="$(ROMFS)"Estensioni .bnr e .icn: i file generati da
bannertoolhanno le estensioni.bnr(banner) e.icn(icona). Alcuni tutorial usano.bin, ma le estensioni corrette sono.bnre.icn: è quello che fanno i buildtool di riferimento.
13. Trappole ed errori frequenti nello sviluppo homebrew 3DS
Ho sbattuto contro tutti questi muri mentre sviluppavo 2048-3DS, e avrei pagato per avere questa lista sotto mano fin dall’inizio. Ecco gli errori più frustranti e come evitarli: ti risparmieranno parecchie ore di debug mentre crei il tuo primo gioco homebrew per Nintendo 3DS.
Artefatti VRAM: il bug fantasma dello schermo del 3DS
Sintomo: pixel colorati casuali che compaiono sullo schermo, diversi a ogni frame.
Causa: C2D_TargetClear() dimenticata prima di C2D_SceneBegin(). La VRAM del 3DS non viene inizializzata automaticamente: contiene residui del frame precedente o dati qualsiasi.
// SBAGLIATO — artefatti VRAM garantiti
C2D_SceneBegin(top);
render_game();
// CORRETTO — cancella sempre prima di disegnare
C2D_TargetClear(top, couleur_fond);
C2D_SceneBegin(top);
render_game();linearAlloc contro malloc: la trappola della memoria audio del 3DS
Sintomo: nessun suono, oppure rumori spuri.
Causa: i buffer audio sul 3DS vanno allocati con linearAlloc(). La memoria restituita da malloc() non è accessibile dal processore DSP. Per liberarla serve poi linearFree(), non free().
// SBAGLIATO — il DSP non può leggere questa memoria
u8 *audio_buf = malloc(size);
// CORRETTO — memoria lineare accessibile dal DSP
u8 *audio_buf = linearAlloc(size);
// ... utilizzo ...
linearFree(audio_buf);DSP_FlushDataCache dimenticata: audio corrotto sul 3DS
Sintomo: suono corrotto, fuori sincrono o completamente sbagliato.
Causa: la cache della CPU e la memoria DSP del 3DS non sono coerenti. Dopo ogni scrittura in un buffer audio devi svuotare esplicitamente la cache:
fread(snd->data, 1, chunk_size, f);
DSP_FlushDataCache(snd->data, snd->size); // OBBLIGATORIO!Formato audio sbagliato: silenzio totale
Sintomo: nessun suono, oppure crash al caricamento.
Causa: il file WAV è compresso (ADPCM, MP3 incapsulato e simili) invece di essere PCM grezzo. Il 3DS supporta solo il PCM non compresso. Verifica e converti:
# Verifica il codec del file WAV
ffprobe music.wav 2>&1 | grep "Audio:"
# Deve mostrare: pcm_s16le (16 bit) o pcm_u8 (8 bit)
# Converte un MP3 in WAV PCM compatibile 3DS
ffmpeg -i music.mp3 -acodec pcm_s16le -ar 22050 music.wavC2D_Color32 non è constexpr in C99
Sintomo: errore di compilazione “initializer element is not constant” con citro2d in C99.
Causa: C2D_Color32() in C è una funzione inline, non un’espressione costante. Non puoi usarla per inizializzare variabili globali in C99. Soluzione:
// SBAGLIATO in C99
static u32 my_color = C2D_Color32(0xFF, 0x00, 0x00, 0xFF);
// CORRETTO — macro MAKE_COLOR al posto di C2D_Color32
#define MAKE_COLOR(r,g,b,a) \
((u32)(r) | ((u32)(g)<<8) | ((u32)(b)<<16) | ((u32)(a)<<24))
static u32 my_color = MAKE_COLOR(0xFF, 0x00, 0x00, 0xFF);Il .3dsx funziona ma il .cia crasha: permessi RSF
Sintomo: l’homebrew funziona alla perfezione in .3dsx dall’Homebrew Launcher, ma crasha subito in .cia (spesso con il messaggio “ErrDisp: An error has occurred” oppure “SD Card was removed“). In Citra il .cia funziona.
Causa: il formato .3dsx eredita i permessi ampi dell’Homebrew Launcher. Il formato .cia ha permessi propri, definiti nel file RSF. L’emulatore Citra ignora le restrizioni sui permessi: ecco perché il .cia gira in emulazione ma non su hardware. Le tre cause principali:
- SystemCallAccess incompleto: se un SVC usato da libctru non è dichiarato, il kernel ARM11 rifiuta la chiamata e il processo viene terminato. Includi tutti gli SVC da 1 a 125.
- ServiceAccessControl mancante: se un servizio (per esempio
dsp::DSPogsp::Gpu) non è dichiarato,srvGetServiceHandle()fallisce e l’init della libreria corrispondente crasha. Includi tutti i servizi elencati nella sezione 12. - Logo: None nell’RSF: su alcuni firmware l’assenza del logo nell’ExeFS provoca un crash all’avvio. Usa
Logo: Nintendocon il flag-exefslogo.
Soluzione: usa il template RSF completo della sezione 12, con il set completo di SVC, servizi e permessi. È il modo più affidabile per avere un .cia funzionante su hardware reale.
ndspWaveBuf non reinizializzato: il suono parte una volta sola
Sintomo: un effetto sonoro parte correttamente la prima volta, poi mai più.
Causa: dopo la riproduzione lo status del ndspWaveBuf resta su “done”. Prima di riprodurlo di nuovo devi reinizializzare lo status e svuotare di nuovo la cache:
snd->wave_buf.status = NDSP_WBUF_FREE; // reinizializza
DSP_FlushDataCache(snd->data, snd->size); // svuota di nuovo la cache
ndspChnWaveBufAdd(channel, &snd->wave_buf); // riproduci di nuovoromfsInit() dimenticata: nessun asset si carica
Sintomo: tutte le fopen("romfs:/...") restituiscono NULL. Font, texture e suoni non si caricano.
Causa: romfsInit() non è stata chiamata all’inizio del programma. Questa chiamata è obbligatoria prima di qualsiasi accesso al file system RomFS.
Dimensioni sbagliate degli asset del banner CIA
Per il formato CIA le dimensioni sono rigide:
- Icona: PNG di 48×48 pixel esatti
- Banner: PNG di 256×128 pixel esatti
- Audio del banner: WAV PCM, preferibilmente breve (circa 3 secondi)
Se le dimensioni non corrispondono, bannertool fallisce in silenzio oppure produce un binario corrotto che fa crashare l’installazione.
14. Conclusione: creare il proprio gioco homebrew per Nintendo 3DS
Sviluppare un gioco homebrew per Nintendo 3DS è un progetto che tocca tanti ambiti interessanti della programmazione: programmazione di sistemi embedded, grafica 2D con citro2d, audio di basso livello con NDSP, gestione della memoria specifica e cross-compilazione ARM. I vincoli della piattaforma (memoria limitata, doppio schermo, formati proprietari) ti obbligano a scrivere codice pulito ed efficiente, e sinceramente mi sono divertito parecchio a raccogliere la sfida.
Le 6 regole d’oro dello sviluppo homebrew 3DS
- Separa la logica di gioco dal rendering. La logica portabile in un file indipendente, il rendering 3DS in un altro. È la strategia che mi ha permesso di sviluppare 2048-3DS in modo efficiente.
- C2D_TargetClear prima di ogni scena. Sempre. Senza eccezioni. Altrimenti arrivano gli artefatti VRAM.
- linearAlloc per l’audio, mai malloc. E non dimenticare mai
DSP_FlushDataCache(). - Solo WAV PCM per tutti i file audio. Converti i tuoi MP3/OGG prima di integrarli.
- Testa il .cia oltre al .3dsx. I permessi sono diversi e un servizio dimenticato nell’RSF manda in crash tutto.
- Dichiara tutti i servizi di sistema nel file RSF, in particolare
dsp::DSP,csnd:SND,hid:USERefs:USER.
Il progetto 2048 per Nintendo 3DS mette in pratica ogni concetto di questo tutorial: rendering su due schermi, animazioni fluide, audio multitraccia, salvataggio binario, localizzazione in 13 lingue, sistema di achievement e packaging in .3dsx e .cia. L’ho reso open source proprio perché serva da base ai tuoi progetti.
Scarica 2048 per Nintendo 3DS, disponibile gratuitamente in .3dsx e .cia. Dai un’occhiata al codice sorgente per vedere tutti questi concetti in azione. E se ti lanci in un progetto homebrew 3DS, condividilo: la community è accogliente e sempre pronta a dare una mano.
Risorse per lo sviluppo homebrew 3DS
- devkitPro:
https://devkitpro.org/, Toolchain ufficiale per lo sviluppo homebrew 3DS - Documentazione libctru:
https://libctru.devkitpro.org/, Riferimento completo dell’API libctru - Header citro2d: i file header in
$DEVKITARM/../libctru/include/citro2d/sono la migliore documentazione della libreria grafica - Wiki 3dbrew:
https://www.3dbrew.org/, Wiki tecnico esaustivo sul Nintendo 3DS (hardware, formati, servizi di sistema) - Esempi ufficiali:
https://github.com/devkitPro/3ds-examples, Esempi su grafica, audio e rete - bannertool:
https://github.com/Steveice10/bannertool, Generatore di banner e icone per il formato CIA - makerom:
https://github.com/3DSGuy/Project_CTR, Assemblatore di file CIA/CCI per Nintendo 3DS
Specifiche tecniche del Nintendo 3DS
- CPU: ARM11 MPCore (ARMv6K) @ 268 MHz, 2 core (4 su New 3DS)
- GPU: DMP PICA200 @ 268 MHz
- RAM: 128 MB (256 MB su New 3DS), di cui 6 MB di VRAM dedicata
- Schermo superiore: 400×240 pixel (800×240 in modalità 3D stereoscopica)
- Schermo inferiore: 320×240 pixel, touch resistivo
- Audio: 24 canali DSP, uscita stereo


