Vous voulez créer votre propre jeu homebrew pour Nintendo 3DS ? Bonne nouvelle : c’est encore tout à fait possible, et c’est un projet vraiment fun. Dans ce tutoriel, je partage tout ce que j’ai appris en développant 2048 pour Nintendo 3DS — un jeu homebrew complet avec animations, audio, 13 langues et un système de succès — de la première ligne de code à la génération d’un fichier .cia installable.
Découvrez le résultat final : 2048 pour Nintendo 3DS — disponible en téléchargement gratuit en .3dsx et .cia.
Ce guide couvre le pipeline complet du développement homebrew 3DS : graphismes 2D avec citro2d, audio NDSP, écran tactile, système de fichiers RomFS, et packaging en CIA. Tout le code est en C99. Pas de blabla théorique — uniquement du code qui tourne, tiré directement du projet 2048-3DS.
Sommaire
- Qu’est-ce qu’un homebrew 3DS
- Prérequis : installer devkitARM et les outils de développement 3DS
- Architecture d’un projet homebrew : séparation logique et rendu
- Gérer le double écran de la Nintendo 3DS
- Rendu graphique 2D avec citro2d
- Gestion des entrées : D-pad, circle pad et écran tactile
- Audio homebrew 3DS : NDSP et format WAV PCM
- Système de fichiers : RomFS et carte SD
- Sauvegarde de données sur carte SD
- Le Makefile 3DS : cross-compilation ARM avec devkitARM
- Générer un fichier .3dsx pour le Homebrew Launcher
- Construire un fichier .cia installable pour le menu HOME
- Pièges et erreurs courantes en développement homebrew 3DS
- Conclusion et ressources pour aller plus loin
1. Qu’est-ce qu’un homebrew Nintendo 3DS ?
Un homebrew 3DS est une application non-officielle exécutée sur une console Nintendo 3DS équipée d’un custom firmware (CFW) comme Luma3DS. La scène homebrew 3DS permet de créer et distribuer librement des jeux, des émulateurs et des utilitaires pour cette console portable.
La Nintendo 3DS dispose d’un processeur ARM11 (ARMv6K) cadencé à 268 MHz et d’un GPU PICA200, avec une particularité unique : deux écrans. L’écran supérieur affiche 400×240 pixels (800×240 en 3D stéréoscopique) et l’écran inférieur, tactile, affiche 320×240 pixels.
Il existe deux formats de distribution pour les homebrews 3DS :
- .3dsx : format homebrew lancé via le Homebrew Launcher. Pas d’installation, exécution directe depuis la carte SD.
- .cia : format installable qui apparaît dans le menu HOME de la 3DS, avec icône et bannière animée. Nécessite un custom firmware (CFW).
Pour 2048-3DS, je génère les deux formats pour toucher un maximum de joueurs. La communauté homebrew 3DS est encore bien vivante grâce à un écosystème d’outils solides : devkitARM (toolchain), libctru (bibliothèque système), citro2d/citro3d (graphismes). Si vous cherchez un bon terrain de jeu pour apprendre la programmation système embarquée ou le développement de jeux rétro, la 3DS est parfaite pour ça.
2. Prérequis : installer devkitARM et les outils de développement 3DS
Avant de commencer à programmer un jeu pour Nintendo 3DS, on doit installer la toolchain de cross-compilation ARM et les bibliothèques spécifiques à la console.
Toolchain devkitARM : le compilateur pour Nintendo 3DS
devkitARM est la toolchain GCC de cross-compilation pour processeurs ARM, fournie par le projet devkitPro. Elle inclut le compilateur, le linker et les outils de build nécessaires pour compiler du code C pour la 3DS. Voici comment l’installer :
# Installation sur Linux/WSL (recommandé pour le développement 3DS)
wget https://apt.devkitpro.org/install-devkitpro-pacman
chmod +x install-devkitpro-pacman
sudo ./install-devkitpro-pacman
# Installer les packages de développement 3DS
sudo dkp-pacman -S 3ds-dev
# Variable d'environnement obligatoire
export DEVKITARM=/opt/devkitpro/devkitARM
Bibliothèques essentielles pour le développement homebrew 3DS
- libctru : bibliothèque C bas niveau pour les services système 3DS (HID, filesystem, audio, GPU). C’est le fondement de tout programme homebrew 3DS.
- citro3d : abstraction au-dessus du GPU PICA200. Gère les render targets, le framebuffer et la synchronisation graphique.
- citro2d : surcouche 2D au-dessus de citro3d. Fournit des primitives simples (rectangles, cercles, ellipses, lignes, texte, sprites) idéales pour les jeux 2D sur 3DS.
Outils complémentaires pour le packaging
- bannertool : génère les fichiers banner.bin et icon.bin nécessaires au format CIA
- makerom : assemble le fichier .cia final à partir de l’ELF, du RSF, du banner et de l’icône
- tex3ds : convertit des images PNG en textures .t3x lisibles par citro2d sur le GPU PICA200
- mkbcfnt : convertit des polices TTF en format .bcfnt pour le rendu texte sur 3DS (supporte Unicode, CJK, cyrillique)
3. Architecture d’un projet homebrew 3DS : séparation logique et rendu
Le piège classique quand on débute en développement homebrew, c’est d’écrire tout le code directement avec les API de la console. Résultat, impossible de tester ou déboguer correctement. Je suis passé par là, et le conseil que je donnerais à tout le monde, c’est de séparer la logique de jeu du rendu graphique dès le départ.
C’est exactement la stratégie utilisée pour 2048-3DS : la logique du jeu 2048 (grille, mouvements, fusions, score) est 100% portable, tandis que le rendu est spécifique à chaque plateforme.
Structure de fichiers du projet 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)
Code portable : la logique du jeu 2048 sans dépendances
Le fichier logic.c de 2048-3DS n’inclut aucun header spécifique à la Nintendo 3DS. Il utilise uniquement les headers C standard (<stdint.h>, <stdlib.h>, <string.h>, <time.h>). Le fichier main_3ds.c appelle les fonctions publiques de la logique :
// logic.h — interface portable du jeu 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; // position avant le mouvement
int to_r, to_c; // position après le mouvement
uint16_t value; // valeur affichée pendant l'animation
int merged; // 1 si fusion
} TileAnim;
typedef struct {
uint16_t cells[GRID_SIZE][GRID_SIZE]; // grille 4x4
uint32_t score;
int won, over;
TileAnim anims[MAX_TILE_ANIMS]; // données d'animation du dernier coup
int anim_count;
int spawn_r, spawn_c; // position de la nouvelle tuile
uint16_t spawn_val; // valeur (2 à 90%, 4 à 10%)
} Game;
void game_init(Game *g); // grille vide + 2 tuiles
int game_move(Game *g, Direction dir); // retourne 1 si la grille a change
int game_is_over(Game *g); // aucun mouvement possible
int game_has_won(Game *g); // tuile >= 2048 atteinte
Le Makefile 3DS compile logic.c + achievements.c + main_3ds.c en filtrant explicitement main_sdl.c. La logique de jeu reste identique quel que soit le front-end de rendu.
4. Gérer le double écran de la Nintendo 3DS
La particularité qui distingue la Nintendo 3DS de toute autre console portable est son double écran. Bien l’exploiter est essentiel pour créer une bonne expérience homebrew.
- Écran supérieur (top) : 400×240 pixels, non tactile. Idéal pour le gameplay principal. Dans 2048-3DS, c’est ici que j’affiche la grille 4×4 avec les animations de tuiles.
- Écran inférieur (bottom) : 320×240 pixels, tactile. Idéal pour l’interface utilisateur, les menus et les contrôles. Dans 2048-3DS, j’y affiche le score, les boutons, les paramètres et les succès.
Initialisation des render targets avec citro2d
// Initialisation du système graphique 3DS
gfxInitDefault();
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
C2D_Prepare();
// Créer un render target pour chaque écran
C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);
Boucle de rendu double écran
À chaque frame, il faut dessiner sur les deux écrans séparément. Voici la structure de la boucle de rendu utilisée dans 2048-3DS :
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
// Écran supérieur — grille du jeu 2048
C2D_TargetClear(top, couleur_fond);
C2D_SceneBegin(top);
render_top_screen(&game, anim_phase, progress);
// Écran inférieur — UI (score, boutons, paramètres)
C2D_TargetClear(bot, col_bot_bg);
C2D_SceneBegin(bot);
render_bot_game(&game, best_score, 0, music_muted);
C3D_FrameEnd(0);
Piège critique du rendu 3DS : C2D_TargetClear est obligatoire. Si vous ne faites pas
C2D_TargetClear()avant chaque scène, vous aurez des artefacts visuels aléatoires — des résidus VRAM de la frame précédente. La VRAM de la 3DS n’est pas initialisée automatiquement. Ce bug est extrêmement déroutant pour les débutants en homebrew car les artefacts changent à chaque frame.
Dimensions des écrans : constantes essentielles
#define TOP_W 400 // largeur écran supérieur
#define TOP_H 240 // hauteur écran supérieur
#define BOT_W 320 // largeur écran inférieur (tactile)
#define BOT_H 240 // hauteur écran inférieur
Attention : sur la console physique, l’écran du bas est centré horizontalement sous l’écran du haut (400 vs 320 pixels de large). Les coordonnées tactiles correspondent directement aux pixels de l’écran inférieur, avec (0,0) en haut à gauche.
5. Rendu graphique 2D avec citro2d sur Nintendo 3DS
citro2d est la bibliothèque de rendu 2D de référence pour les jeux homebrew 3DS. Construite au-dessus de citro3d et du GPU PICA200, elle gère en interne le batching des primitives et la gestion des textures GPU. Voici comment l’utiliser, avec des exemples concrets tirés de 2048-3DS.
Dessiner des primitives : rectangles, cercles, ellipses et lignes
// Rectangle plein (tuiles du jeu 2048)
C2D_DrawRectSolid(x, y, z, largeur, hauteur, couleur);
// Cercle plein (coins arrondis, icônes)
C2D_DrawCircleSolid(centre_x, centre_y, z, rayon, couleur);
// Ellipse pleine (icône musicale dans 2048-3DS)
C2D_DrawEllipseSolid(x, y, z, largeur, hauteur, couleur);
// Ligne (barre de mute audio)
C2D_DrawLine(x1, y1, couleur1, x2, y2, couleur2, epaisseur, z);
Le paramètre z contrôle la profondeur (0.0f pour le plan standard). Les couleurs sur 3DS utilisent le format ABGR en mémoire (attention, c’est l’inverse du ARGB classique). citro2d fournit la macro C2D_Color32(r, g, b, a) pour construire les couleurs correctement :
// Piège C99 : C2D_Color32() n'est pas constexpr
// Utiliser un macro pour les constantes de couleur globales
#define MAKE_COLOR(r,g,b,a) \
((u32)(r) | ((u32)(g)<<8) | ((u32)(b)<<16) | ((u32)(a)<<24))
// Palette de couleurs du jeu 2048 sur 3DS
#define col_grid_bg MAKE_COLOR(0xBB, 0xAD, 0xA0, 0xFF) // fond grille
#define col_bot_bg MAKE_COLOR(0xFA, 0xF8, 0xEF, 0xFF) // fond écran bas
#define col_text_dk MAKE_COLOR(0x77, 0x6E, 0x65, 0xFF) // texte sombre
#define col_text_lt MAKE_COLOR(0xF9, 0xF6, 0xF2, 0xFF) // texte clair
Rectangles arrondis : une astuce graphique pour citro2d
citro2d ne fournit pas de primitive « rectangle arrondi ». Pour les tuiles du jeu 2048 et les boutons de l’interface, j’ai créé cette technique en combinant un rectangle central, deux rectangles latéraux et quatre cercles aux coins :
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;
}
// Corps central + bords lateraux
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 cercles de rayon r aux coins
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);
}
Cette fonction est utilisée partout dans 2048-3DS : tuiles de jeu, boîtes de score, boutons, sliders de volume, boîtes de dialogue de confirmation.
Afficher du texte sur Nintendo 3DS avec le format .bcfnt
Le rendu de texte sur 3DS passe par le format .bcfnt (Binary CTR Font). Chaque chaîne doit être « parsée » dans un buffer de texte avant d’être dessinée. Dans 2048-3DS, cette technique est utilisée pour afficher les scores, les noms de boutons en 13 langues, et les notifications de succès :
// Initialisation des buffers de texte
C2D_TextBuf s_dynamicBuf = C2D_TextBufNew(512);
C2D_Font s_font = C2D_FontLoad("romfs:/font.bcfnt");
// Fonction utilitaire : dessiner du texte centré
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);
}
// Important : vider le buffer de texte à chaque frame
C2D_TextBufClear(s_dynamicBuf);
Le facteur 0.62f est ajusté pour un centrage vertical visuellement correct du texte.
Sprites et textures GPU au format .t3x
Les images doivent être converties en format .t3x (texture 3DS optimisée pour le GPU PICA200) avec l’outil tex3ds. Un fichier .t3s décrit le sprite sheet. Dans 2048-3DS, les 8 icônes de succès (32×32 pixels chacune) sont packées dans un seul 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)
// Chargement du sprite sheet depuis RomFS
C2D_SpriteSheet sheet = C2D_SpriteSheetLoad("romfs:/sprites.t3x");
// Affichage d'une icône de succès
C2D_Image img = C2D_SpriteSheetGetImage(sheet, index);
C2D_DrawImageAt(img, x, y, 0.0f, NULL, scale_x, scale_y);
// Affichage en greyscale (succès verrouille)
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);
Animations fluides sur Nintendo 3DS
Pour des animations fluides dans un jeu homebrew 3DS, utilisez osGetTime() (temps en millisecondes) et l’interpolation avec easing. Voici le système d’animation implémenté dans 2048-3DS pour le glissement et l’apparition des tuiles :
// Timing des animations du jeu 2048
#define ANIM_SLIDE_MS 120 // duree du glissement de tuile
#define ANIM_POP_MS 100 // duree du "pop" (fusion/apparition)
typedef enum { ANIM_NONE, ANIM_SLIDING, ANIM_POPPING } AnimPhase;
// Easing quadratique : décélération naturelle
static float ease_out_quad(float t) {
return t * (2.0f - t);
}
// Dans la boucle principale : interpolation des positions
u64 now = osGetTime();
u64 elapsed = now - anim_start;
float progress = (float)elapsed / ANIM_SLIDE_MS;
float t = ease_out_quad(progress);
// Glissement lineaire de la tuile
float cur_x = from_px + (to_px - from_px) * t;
float cur_y = from_py + (to_py - from_py) * t;
L’animation se déroule en deux phases : d’abord le glissement (120 ms), puis le « pop » — les tuiles fusionnées grossissent à 125% avant de revenir à 100%, et la nouvelle tuile apparaît en grandissant de 0 à 100%. Ce système rend le jeu 2048 sur 3DS aussi satisfaisant visuellement que la version originale.
6. Gestion des entrées sur Nintendo 3DS : D-pad, circle pad et écran tactile
La Nintendo 3DS offre plusieurs périphériques d’entrée pour les jeux homebrew, tous accessibles via la bibliothèque libctru (HID). Dans 2048-3DS, j’utilise les trois : D-pad et circle pad pour déplacer les tuiles, écran tactile pour les boutons et sliders.
Lire les boutons physiques du D-pad et des touches A/B/X/Y
hidScanInput(); // Scanner l'etat des entrées (1x par frame)
u32 kDown = hidKeysDown(); // Boutons nouvellement presses cette frame
// D-pad : deplacer les tuiles dans le jeu 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; }
// Boutons système
if (kDown & KEY_B) { /* navigation retour */ }
if (kDown & KEY_START) { break; /* quitter l'application */ }
if (kDown & KEY_SELECT) { game_init(&game); /* nouvelle partie */ }
Circle Pad : le stick analogique de la 3DS avec deadzone
circlePosition cpad;
hidCircleRead(&cpad);
// Deadzone de 80 pour éviter les faux positifs
// Plage du circle pad : environ -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 est critique pour le circle pad 3DS. Sans seuil, le stick enregistre des micro-mouvements en continu et déclenche des mouvements non désirés. Une valeur de 80 (sur une plage d’environ -155 à +155) fonctionne bien en pratique pour un jeu comme 2048.
Écran tactile : gérer les clics sur les boutons de l’interface
L’écran tactile de la 3DS est résistif (pression, pas capacitif). Les coordonnées sont directement en pixels de l’écran inférieur (320×240). Dans 2048-3DS, l’écran tactile gère les boutons « Nouvelle partie », « Succès », « Paramètres », les sliders de volume et la sélection de langue :
if (kDown & KEY_TOUCH) {
touchPosition touch;
hidTouchRead(&touch);
int mx = touch.px; // 0..319
int my = touch.py; // 0..239
// Detection de clic sur le bouton "Nouvelle partie"
float btn_x = (BOT_W - BTN_W) / 2; // centre horizontalement
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;
}
}
Utilisez hidKeysDown() avec KEY_TOUCH pour les appuis ponctuels (boutons), et hidKeysHeld() pour les interactions continues (sliders de volume, drag).
Boucle principale d’un jeu homebrew 3DS
while (aptMainLoop()) {
u64 now = osGetTime();
audio_music_tick(music_volume, music_muted);
hidScanInput();
u32 kDown = hidKeysDown();
if (kDown & KEY_START) break; // Sortie propre
// Traitement des entrées (D-pad, circle pad, tactile)
// Mise a jour de la logique du jeu 2048
// Rendu des deux écrans
// ...
}
aptMainLoop() gère le cycle de vie de l’application homebrew : mise en veille, fermeture par le système, retour au menu HOME.
7. Audio homebrew 3DS : NDSP et format WAV PCM
Croyez-en mon expérience : l’audio sur Nintendo 3DS est l’aspect où j’ai perdu le plus de temps en développement homebrew. Tout semble simple sur le papier, mais les pièges sont partout. Le backend audio de la 3DS est NDSP (Nintendo DSP) : il nécessite le fichier firmware DSP (dspfirm.bin sur la carte SD) et supporte le mixing multi-canal avec interpolation linéaire.
Dans 2048-3DS, j’ai implémenté la musique de fond sur 4 pistes et les effets sonores pour les mouvements de tuiles et les succès.
Format audio pour la 3DS : WAV PCM obligatoire
Piège majeur du développement audio 3DS : la console ne supporte que le format WAV PCM brut (pas de MP3, pas d’OGG, pas de WAV compressé ADPCM). Les fichiers doivent être en PCM 8 ou 16 bits, mono ou stéréo. Tout autre format sera silencieusement ignoré ou causera un crash.
Allocation mémoire pour l’audio 3DS : linearAlloc au lieu de malloc
Les buffers audio sur 3DS doivent être alloués avec linearAlloc() et non malloc(). La mémoire linéaire est directement accessible par le processeur DSP ; la mémoire heap standard ne l’est pas.
// Structure pour stocker un fichier audio WAV sur 3DS
typedef struct {
u8 *data; // Données PCM (allouées avec linearAlloc)
u32 size; // Taille en octets
u32 sample_rate; // Fréquence d'échantillonnage
u16 channels; // 1 (mono) ou 2 (stéréo)
u16 bits_per_sample; // 8 ou 16 bits
ndspWaveBuf wave_buf; // Buffer NDSP
int loaded; // Flag de chargement réussi
} WavSound;
Chargement de fichiers WAV pour le homebrew 3DS
static int wav_load(WavSound *snd, const char *path)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
// Lire et valider le 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;
}
// Parcourir les chunks 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) {
// Extraire channels, sample_rate, bits_per_sample
// ...
} else if (memcmp(chunk_id, "data", 4) == 0) {
// IMPORTANT : linearAlloc, pas malloc !
snd->data = (u8 *)linearAlloc(chunk_size);
fread(snd->data, 1, chunk_size, f);
} else {
fseek(f, chunk_size, SEEK_CUR);
}
}
// Preparer le 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;
// OBLIGATOIRE : flusher le cache CPU vers le DSP
DSP_FlushDataCache(snd->data, snd->size);
return 0;
}
DSP_FlushDataCache est obligatoire après chaque écriture dans un buffer audio 3DS. Sans cet appel, le DSP lit des données corrompues car le cache CPU n’est pas cohérent avec la mémoire DSP. C’est la cause #1 de bugs audio silencieux sur Nintendo 3DS.
Initialisation audio NDSP
Voici l’initialisation audio implémentée dans 2048-3DS :
static void audio_init(void)
{
if (ndspInit() != 0) return;
ndspSetOutputMode(NDSP_OUTPUT_STEREO);
// Canal 0 : musique de fond
// Canal 1 : SFX (mouvement de tuile)
// Canal 2 : SFX (succès debloque)
for (int ch = 0; ch < 3; ch++) {
ndspChnReset(ch);
ndspChnSetInterp(ch, NDSP_INTERP_LINEAR);
ndspChnSetFormat(ch, NDSP_FORMAT_STEREO_PCM16);
}
}
Jouer un son et gérer le volume sur 3DS
// NDSP : ajouter le buffer audio au canal
snd->wave_buf.status = NDSP_WBUF_FREE; // reinitialiser le status
DSP_FlushDataCache(snd->data, snd->size);
ndspChnWaveBufAdd(channel, &snd->wave_buf);
// Volume NDSP : tableau de 12 floats (mix stéréo par canal)
float vol = (float)music_volume / 128.0f; // 0..128 -> 0.0..1.0
float mix[12] = {0};
mix[0] = vol; // canal gauche
mix[1] = vol; // canal droite
ndspChnSetMix(0, mix);
Enchaînement automatique de pistes musicales
Dans 2048-3DS, 4 pistes musicales s’enchaînent automatiquement (intro, music1, music2, music3). À chaque frame, je vérifie si la piste en cours est terminée :
static void audio_music_tick(int music_volume, int music_muted)
{
if (!s_audio_init || music_muted) return;
if (!ndspChnIsPlaying(0)) {
// Passer à la piste suivante (boucle cyclique)
s_music_current = (s_music_current + 1) % MUSIC_TRACK_COUNT;
audio_play_current_track(music_volume, music_muted);
}
}
Nettoyage audio en fin de programme
// Liberer proprement les ressources audio
ndspChnReset(0);
ndspChnReset(1);
ndspChnReset(2);
ndspExit();
// Liberer la mémoire lineaire (pas free, mais 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. Système de fichiers 3DS : RomFS et carte SD
La Nintendo 3DS offre deux systèmes de fichiers accessibles aux applications homebrew : RomFS pour les assets en lecture seule et SDMC pour la lecture/écriture sur carte SD.
RomFS : embarquer les assets dans le binaire homebrew
RomFS (Read-Only Memory FileSystem) permet d’embarquer des fichiers directement dans le binaire .3dsx ou .cia. C’est idéal pour les assets qui ne changent pas : polices, textures, musiques, sprites. Dans 2048-3DS, le dossier romfs/ contient la police .bcfnt avec support CJK, le sprite sheet des succès et 6 fichiers audio WAV :
// Initialisation obligatoire avant tout acces RomFS
romfsInit();
// Acces aux assets avec le préfixe 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");
Le contenu du dossier romfs/ est automatiquement embarqué par le Makefile 3DS :
# Dans Makefile.3ds
ROMFS := romfs
Formats d’assets spécifiques à la Nintendo 3DS
- .bcfnt : police bitmap compilée avec
mkbcfnt. Supporte Unicode complet (CJK, cyrillique, caractères accentués). Dans 2048-3DS, une seule police couvre les 13 langues. - .t3x : texture GPU compilée avec
tex3ds. Format optimisé pour le PICA200. Charge directement en VRAM sans conversion. - .wav : audio PCM brut. Pas de conversion à l’exécution, mais doit être en PCM non compressé (voir section audio).
SDMC : sauvegarder des données sur la carte SD de la 3DS
Pour la sauvegarde de données (scores, paramètres, progression), les homebrews 3DS écrivent sur la carte SD avec le préfixe sdmc:/ :
// Chemins de sauvegarde pour 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"
// Créer le répertoire de sauvegarde au démarrage
#include <sys/stat.h>
mkdir("sdmc:/3ds", 0777);
mkdir(SAVE_DIR, 0777);
L’écriture utilise les fonctions C standard (fopen, fwrite, fclose). Aucune API spécifique à la 3DS n’est nécessaire pour les opérations fichiers sur SDMC.
9. Sauvegarde de données sur carte SD dans un homebrew 3DS
Pour un jeu homebrew 3DS, la sauvegarde binaire est la méthode la plus directe et la plus économique. Voici comment 2048-3DS implémente la persistence des succès et des paramètres.
Sauvegarde binaire du système de succès
Le jeu 2048-3DS comporte 8 succès (paliers de score : 500, 1000, 2500, 5000, 10000, 20000, 50000, 100000 points). La sauvegarde utilise 8 octets — un octet par succès :
// Sauvegarde : 8 octets (0x00 = verrouille, 0x01 = debloque)
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;
}
// Chargement avec valeurs par défaut si le fichier n'existe pas
int achievements_load(Achievements *a, const char *path)
{
achievements_init(a); // tous verrouilles par défaut
FILE *f = fopen(path, "rb");
if (!f) return -1; // première exécution : pas de fichier
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;
}
Sauvegarde des paramètres utilisateur
Les paramètres de 2048-3DS (langue, volume musique, volume effets, état mute) sont sauvegardés dans 4 octets :
// Format : [0] = langue, [1] = volume musique, [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);
}
Avantages de cette approche pour un jeu homebrew : taille fixe, pas de parsing, pas de dépendance à une bibliothèque JSON/XML, lecture instantanée. Les paramètres sont sauvegardés automatiquement quand l’utilisateur quitte le menu paramètres ou l’application.
10. Le Makefile 3DS : cross-compilation ARM avec devkitARM
Le système de build pour les homebrews 3DS repose sur les règles fournies par devkitARM. Comprendre le Makefile 3DS est essentiel pour déboguer les problèmes de compilation.
Structure du Makefile pour un projet homebrew 3DS
# Vérifier que devkitARM est configuré
ifeq ($(strip $(DEVKITARM)),)
$(error "Please set DEVKITARM in your environment")
endif
include $(DEVKITARM)/3ds_rules
# Configuration du projet homebrew
TARGET := 2048
BUILD := build_3ds
SOURCES := source
ROMFS := romfs
# Metadata affichée dans le Homebrew Launcher et le menu HOME
APP_TITLE := 2048
APP_DESCRIPTION := 2048 puzzle game for 3DS
APP_AUTHOR := Gekkode
Flags de compilation ARM pour la 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__
Explication de chaque flag de la cross-compilation ARM pour 3DS :
-march=armv6k: architecture du processeur ARM11 de la Nintendo 3DS-mtune=mpcore: optimise le code pour le core MPCore-mfloat-abi=hard: utilise le VFP (virgule flottante matérielle), crucial pour les performances graphiques-mword-relocations: génère les relocations nécessaires au format 3DSX-ffunction-sections: permet au linker d’éliminer le code mort et réduire la taille du binaire-D__3DS__: définit la macro pour les#ifdefconditionnels entre PC et 3DS
Linking des bibliothèques citro2d et libctru
LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lcitro2d -lcitro3d -lctru -lm
L’ordre des bibliothèques est important pour le linker GNU : -lcitro2d dépend de -lcitro3d, qui dépend de -lctru. Toujours ordonner de la plus haute à la plus basse abstraction.
Filtrage des sources : exclure le rendu PC du build 3DS
# Exclure main_sdl.c du build 3DS (on ne compile que main_3ds.c)
CFILES := $(filter-out main_sdl.c, \
$(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))))
Ce filtrage garantit que seul main_3ds.c est compilé pour le build 3DS.
Commandes de build
# Compiler le homebrew en .3dsx
make -f Makefile.3ds
# Compiler et générer un .cia installable
make -f Makefile.3ds cia
# Nettoyage complet
make -f Makefile.3ds clean
11. Générer un fichier .3dsx pour le Homebrew Launcher
Le format .3dsx est le format homebrew standard de la Nintendo 3DS, lancé via le Homebrew Launcher. C’est le format le plus simple à distribuer : un seul fichier à copier sur la carte SD.
Le Makefile 3DS génère automatiquement deux fichiers :
2048.3dsx: l’exécutable homebrew, avec le RomFS embarqué2048.smdh: les métadonnées SMDH (titre, auteur, description, icône)
SMDH : les métadonnées de votre homebrew 3DS
Le fichier SMDH (Simple Metadata Header) contient les informations affichées dans le Homebrew Launcher :
# Définis dans le Makefile.3ds
APP_TITLE := 2048
APP_DESCRIPTION := 2048 puzzle game for 3DS
APP_AUTHOR := Gekkode
L’icône est un PNG 48×48 pixels, détecté automatiquement si icon.png ou 2048.png existe à la racine du projet.
Embarquer le RomFS dans le .3dsx
Le RomFS est intégré directement dans le fichier .3dsx. L’utilisateur final n’a qu’un seul fichier à manipuler :
_3DSXFLAGS += --romfs=$(CURDIR)/romfs
12. Construire un fichier .cia installable pour le menu HOME de la 3DS
Le format .cia (CTR Importable Archive) est le format installable de la Nintendo 3DS. Une fois installé via FBI ou un autre gestionnaire de titres, le jeu apparaît dans le menu HOME de la console avec une icône personnalisée, une bannière animée et un son de lancement.
C’est le format utilisé pour distribuer 2048 pour Nintendo 3DS en version .cia.
Attention : c’est l’étape la plus délicate du développement homebrew 3DS, et de loin. J’ai passé un temps fou à comprendre pourquoi un fichier .cia pouvait fonctionner parfaitement dans l’émulateur Citra et crasher immédiatement sur une vraie 3DS. La cause est presque toujours un fichier RSF mal configuré. Ce qui suit est le résultat de nombreuses heures de tests sur du vrai hardware — je détaille chaque paramètre pour que vous n’ayez pas à galérer.
Assets requis pour construire un .cia
- Icône : PNG 48×48 pixels exactement (affichée dans le menu HOME)
- Image de bannière : PNG 256×128 pixels exactement (affichée en haut de l’écran quand le jeu est sélectionné)
- Son de bannière : WAV PCM 16-bit, 44100 Hz, stéréo, environ 3 secondes (joué quand le jeu est sélectionné dans le menu HOME). Le format est strict — utilisez
ffmpegpour convertir :
# Convertir n'importe quel audio en WAV compatible bannertool
ffmpeg -i music.mp3 -acodec pcm_s16le -ar 44100 -ac 2 -t 3 banner.wav
Étapes de construction d’un fichier CIA pour la 3DS
La construction d’un .cia se fait en 4 étapes. L’approche recommandée est d’utiliser un RSF template avec des variables $(VARIABLE) substituées par les flags -DVARIABLE="valeur" de makerom. Cette méthode permet de séparer la configuration (dans le Makefile) du template de permissions (dans le RSF), ce qui rend le build plus propre et réutilisable :
# 1. Construire le binaire ELF
make -f Makefile.3ds
# 2. Générer le banner (image + audio de lancement)
bannertool makebanner \
-i assets/banner.png \
-a assets/music/banner.wav \
-o build_3ds/banner.bnr
# 3. Générer l'icône SMDH pour le menu HOME
bannertool makesmdh \
-s "2048" \
-l "2048 - puzzle game for 3DS" \
-p "Gekkode" \
-i icon.png \
-o build_3ds/icon.icn
# 4. Assembler le .cia final avec 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"
Chaque flag -DXXX="valeur" remplace la variable $(XXX) correspondante dans le fichier RSF. Ce système de substitution permet d’avoir un unique template RSF réutilisable pour tous vos projets — seuls les flags -D changent d’un projet à l’autre.
Les flags makerom importants :
-target t: cible « test » (pour les homebrews non signés par Nintendo)-exefslogo: inclut le logo dans l’ExeFS (nécessaire pour le splash screen de démarrage)-major / -minor / -micro: version du titre (affichée dans les paramètres système)
Le fichier RSF : configurer les permissions de votre homebrew CIA
Le fichier RSF (ROM Specification File) est le fichier le plus critique du build CIA. Il définit les métadonnées, les permissions système, les services autorisés et les appels système disponibles. Un RSF incomplet = crash immédiat sur vrai hardware, même si tout fonctionne dans Citra.
Voici le template RSF complet utilisé pour 2048-3DS, mis au point après de nombreux tests sur du vrai hardware. Chaque section est expliquée ensuite :
BasicInfo : identité du titre
BasicInfo:
Title : $(APP_TITLE)
ProductCode : $(APP_PRODUCT_CODE)
Logo : Nintendo
Title: le nom affiché dans les paramètres système. Utilise une variable$(APP_TITLE)substituée par-DAPP_TITLE="2048".ProductCode: un identifiant au formatCTR-H-XXXX. LeHsignifie homebrew. Choisissez un code unique (ex.CTR-H-2048).Logo: le splash screen animé affiché au démarrage.Nintendo: le logo animé officiel 3DS (recommandé — c’est le comportement standard)Homebrew: le logo de la communauté homebrewLicensed/Distributed: variantes du logo NintendoNone: à éviter — peut provoquer des crashes sur certaines versions firmware
RomFs : ressources embarquées
RomFs:
RootPath: $(APP_ROMFS)
Chemin vers le dossier romfs/ contenant les assets embarqués (sprites, polices, audio). Passé via -DAPP_ROMFS="romfs". Si votre application n’a pas de RomFS, vous pouvez omettre cette section.
TitleInfo : identification unique
TitleInfo:
Category : $(APP_CATEGORY)
UniqueId : $(APP_UNIQUE_ID)
Category: toujoursApplicationpour un jeu ou utilitaire homebrew.UniqueId: identifiant hexadécimal unique dans la plage0xF0000à0xFFFFF(plage réservée aux homebrews). Chaque homebrew installé sur la console doit avoir un UniqueId différent pour éviter les conflits. Pour 2048, j’utilise0xF2048.
Option : options de packaging
Option:
UseOnSD : $(APP_USE_ON_SD)
FreeProductCode : true
MediaFootPadding : false
EnableCrypt : $(APP_ENCRYPTED)
EnableCompress : true
UseOnSD:true(obligatoire pour un homebrew installé sur carte SD).FreeProductCode:truepour que makerom accepte un ProductCode libre (sans vérification du format Nintendo).EnableCrypt:false(les homebrews n’ont pas de clés de chiffrement Nintendo).EnableCompress:truepour compresser la section .code de l’ExeFS (réduit la taille du .cia).
AccessControlInfo : la section critique
C’est ici que 90% des crashes CIA se jouent. Cette section définit tout ce que votre application a le droit de faire sur la console. Si un seul appel système ou service manque, le CIA crashe instantanément sur vrai hardware — alors que Citra ignore ces restrictions.
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 : true
Les paramètres à adapter selon votre projet :
MemoryType:Applicationpour un jeu/utilitaire standard.Systempour un module système.SystemMode: quantité de RAM allouée.64MBest le standard. Utilisez96MBuniquement pour les applications exclusives New 3DS nécessitant plus de mémoire.SystemModeExt:Legacypour une compatibilité Old 3DS + New 3DS.CpuSpeed:268MHzactive le boost de fréquence sur New 3DS (retombe à 268 MHz sur Old 3DS, c’est géré automatiquement).EnableL2Cache:falsepar défaut. Mettre àtruepour les applications CPU-intensives sur New 3DS (peut causer des instabilités sur Old 3DS).
Ensuite viennent les quatre sous-sections de permissions. Mon conseil : incluez le set complet. Un homebrew basique n’utilise pas tous ces SVCs et services, mais en déclarer trop ne pose aucun problème de performance ni de sécurité sur une console avec CFW — en revanche, en oublier un seul provoque un crash immédiat :
FileSystemAccess : accès au système de fichiers
FileSystemAccess:
- CategorySystemApplication
- CategoryHardwareCheck
- CategoryFileSystemTool
- Debug
- TwlCardBackup
- TwlNandData
- Boss
- DirectSdmc
- Core
- CtrNandRo
- CtrNandRw
- CtrNandRoWrite
- CategorySystemSettings
- CardBoard
- ExportImportIvs
- DirectSdmcWrite
- SwitchCleanup
- SaveDataMove
- Shop
- Shell
- CategoryHomeMenu
- SeedDB
Les plus importants pour un homebrew basique : DirectSdmc + DirectSdmcWrite (lecture/écriture SD) et Core. Mais il est plus sûr d’inclure le set complet.
IoAccessControl : accès I/O bas niveau
IoAccessControl:
- FsMountNand
- FsMountNandRoWrite
- FsMountTwln
- FsMountWnand
- FsMountCardSpi
- UseSdif3
- CreateSeed
- UseCardSpi
SystemCallAccess : appels système ARM11 (SVCs)
Cette liste définit quels appels système (supervisor calls) votre application peut utiliser. C’est la section la plus critique : si un SVC manque et que libctru l’appelle, la console crashe immédiatement. Incluez tous les SVCs de 1 à 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 : 125
Important : les numéros ne sont pas continus — il y a des trous (63-70, 89-95, 110-111, 116). C’est normal, ce sont des SVCs réservés ou non implémentés par le kernel 3DS.
ServiceAccessControl : services système
Les services sont les API haut niveau de la 3DS. Chaque bibliothèque de libctru utilise un ou plusieurs services. Si un service n’est pas déclaré ici, l’appel à srvGetServiceHandle() échouera et l’init de la bibliothèque correspondante crashera :
ServiceAccessControl:
- APT:U # Cycle de vie de l'application (aptMainLoop)
- ac:u # Configuration réseau
- am:net # Application Manager (installation de titres)
- boss:U # SpotPass
- cam:u # Camera
- cecd:u # StreetPass
- cfg:nor # Configuration NOR
- cfg:u # Configuration système (langue, region)
- csnd:SND # Audio CSND
- dsp::DSP # Audio NDSP (backend principal)
- frd:u # Liste d'amis
- fs:USER # Système de fichiers (SDMC, RomFS)
- gsp::Gpu # GPU PICA200 (graphismes)
- gsp::Lcd # Contrôle de l'écran LCD
- hid:USER # Input (boutons, pad, tactile)
- http:C # HTTP client
- ir:rst # Infrarouge (C-stick New 3DS)
- ir:u # Infrarouge générique
- ir:USER # Infrarouge utilisateur
- mic:u # Microphone
- mcu::HWC # Microcontrôleur hardware
- ndm:u # Network Daemon Manager
- news:s # Notifications
- nwm::EXT # Network Manager étendu
- nwm::UDS # Network Manager UDS (local wireless)
- ptm:sysm # Power/Timer Manager système
- ptm:u # Power/Timer Manager utilisateur
- pxi:dev # PXI device access
- soc:U # Sockets (réseau TCP/UDP)
- ssl:C # SSL/TLS
- y2r:u # Conversion YUV vers RGB
Pour un jeu homebrew basique, les services indispensables sont : APT:U, fs:USER, gsp::Gpu, hid:USER. Ajoutez dsp::DSP si vous utilisez l’audio, cfg:u si vous lisez la configuration système. Mais comme pour les SVCs, il est plus sûr d’inclure le set complet — il n’y a aucune pénalité à déclarer un service non utilisé.
SystemControlInfo : pile, sauvegarde et dépendances
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: 0x0004013000002f02
SaveDataSize:0KBsi vous sauvegardez directement sur la SD (ce que font la plupart des homebrews). Mettez une taille si vous utilisez l’API de sauvegarde système.StackSize:0x40000(256 KB) est un défaut solide. Augmentez si votre app fait de la récursion lourde ou alloue de gros tableaux sur la pile.Dependency: les title IDs des modules système dont dépend votre application. Attention : les title IDs ne doivent PAS avoir de suffixeL. Écrire0x0004013000002402Lau lieu de0x0004013000002402peut causer des erreurs silencieuses. Reprenez la liste ci-dessus telle quelle.
IORegisterMapping et MemoryMapping
IORegisterMapping:
- 1ff00000-1ff7ffff
MemoryMapping:
- 1f000000-1f5fffff:r
Ces sections définissent les plages de mémoire et registres I/O accessibles. Reprenez les valeurs ci-dessus sans modification — elles couvrent les besoins de tous les homebrews standards.
Adapter le RSF à votre projet
Le template RSF ci-dessus est générique et réutilisable. Pour l’adapter à un nouveau projet, il suffit de changer les flags -D dans la commande makerom :
Flag -D | Valeur 2048 | À adapter |
|---|---|---|
APP_TITLE | 2048 | Nom de votre app |
APP_PRODUCT_CODE | CTR-H-2048 | CTR-H-XXXX unique |
APP_UNIQUE_ID | 0xF2048 | Hex unique dans 0xF0000-0xFFFFF |
APP_ROMFS | romfs | Chemin de votre dossier romfs |
APP_SYSTEM_MODE | 64MB | 96MB si exclusif New 3DS |
APP_VERSION_MAJOR | 1 | Numéro de version majeure |
Les autres valeurs (APP_ENCRYPTED=false, APP_CATEGORY=Application, APP_USE_ON_SD=true, etc.) sont les mêmes pour tous les homebrews.
Automatiser le build CIA dans le Makefile
# Variables en haut du 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
# Cible CIA — un seul "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)"
Extensions .bnr et .icn : les fichiers générés par
bannertoolont les extensions.bnr(banner) et.icn(icon). Certains tutoriels utilisent.bin, mais les extensions correctes sont.bnret.icn— c’est ce que font les buildtools de référence.
13. Pièges et erreurs courantes en développement homebrew 3DS
Je me suis pris tous ces murs pendant le développement de 2048-3DS, et j’aurais adoré avoir cette liste sous la main dès le départ. Voici les erreurs les plus frustrantes et comment les éviter — ça vous épargnera pas mal d’heures de debug en créant votre premier jeu homebrew pour Nintendo 3DS.
Artefacts VRAM : le bug fantôme de l’écran 3DS
Symptôme : des pixels colorés aléatoires apparaissent sur l’écran, différents à chaque frame.
Cause : oubli de C2D_TargetClear() avant C2D_SceneBegin(). La VRAM de la 3DS n’est pas initialisée automatiquement — elle contient des résidus de la frame précédente ou des données quelconques.
// MAUVAIS — artefacts VRAM garantis
C2D_SceneBegin(top);
render_game();
// CORRECT — toujours effacer avant de dessiner
C2D_TargetClear(top, couleur_fond);
C2D_SceneBegin(top);
render_game();
linearAlloc vs malloc : le piège de la mémoire audio 3DS
Symptôme : le son est muet ou des bruits parasites sont émis.
Cause : les buffers audio sur 3DS doivent être alloués avec linearAlloc(). La mémoire retournée par malloc() n’est pas accessible par le processeur DSP. Il faut aussi utiliser linearFree() pour libérer (pas free()).
// MAUVAIS — le DSP ne peut pas lire cette mémoire
u8 *audio_buf = malloc(size);
// CORRECT — mémoire lineaire accessible par le DSP
u8 *audio_buf = linearAlloc(size);
// ... utilisation ...
linearFree(audio_buf);
DSP_FlushDataCache oublié : audio corrompu sur 3DS
Symptôme : le son est corrompu, désynchro, ou complètement faux.
Cause : le cache CPU et la mémoire DSP de la 3DS ne sont pas cohérents. Il faut explicitement flusher le cache après avoir écrit dans un buffer audio :
fread(snd->data, 1, chunk_size, f);
DSP_FlushDataCache(snd->data, snd->size); // OBLIGATOIRE !
Format audio incorrect : silence complet
Symptôme : aucun son, ou crash au chargement.
Cause : le fichier WAV est compressé (ADPCM, MP3 encapsulé, etc.) au lieu d’être en PCM brut. La 3DS ne supporte que le PCM non compressé. Vérifier et convertir :
# Vérifier le codec du fichier WAV
ffprobe music.wav 2>&1 | grep "Audio:"
# Doit afficher : pcm_s16le (16 bits) ou pcm_u8 (8 bits)
# Convertir un MP3 en WAV PCM compatible 3DS
ffmpeg -i music.mp3 -acodec pcm_s16le -ar 22050 music.wav
C2D_Color32 non constexpr en C99
Symptôme : erreur de compilation « initializer element is not constant » avec citro2d en C99.
Cause : C2D_Color32() est une fonction inline en C, pas une expression constante. Impossible de l’utiliser pour initialiser des variables globales en C99. Solution :
// MAUVAIS en C99
static u32 my_color = C2D_Color32(0xFF, 0x00, 0x00, 0xFF);
// CORRECT — macro MAKE_COLOR au lieu de 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);
Le .3dsx fonctionne mais le .cia plante : permissions RSF
Symptôme : le homebrew fonctionne parfaitement en .3dsx via le Homebrew Launcher, mais plante immédiatement en .cia (souvent avec le message « ErrDisp: An error has occurred » ou « SD Card was removed« ). Le .cia fonctionne dans Citra.
Cause : le format .3dsx hérite des permissions larges du Homebrew Launcher. Le format .cia a ses propres permissions définies dans le fichier RSF. L’émulateur Citra ignore les restrictions de permissions, c’est pourquoi le .cia fonctionne en émulation mais pas sur le hardware. Trois causes principales :
- SystemCallAccess incomplet : si un SVC utilisé par libctru n’est pas déclaré, le kernel ARM11 refuse l’appel et le processus est tué. Incluez tous les SVCs de 1 à 125.
- ServiceAccessControl manquant : si un service (ex.
dsp::DSP,gsp::Gpu) n’est pas déclaré,srvGetServiceHandle()échoue et l’init de la bibliothèque correspondante crashe. Incluez tous les services listés dans la section 12. - Logo: None dans le RSF : sur certains firmwares, l’absence de logo dans l’ExeFS provoque un crash au démarrage. Utilisez
Logo: Nintendoavec le flag-exefslogo.
Solution : utilisez le template RSF complet présenté dans la section 12, avec le set complet de SVCs, services et permissions. C’est la méthode la plus fiable pour garantir un .cia fonctionnel sur du vrai hardware.
ndspWaveBuf pas réinitialisé : le son ne joue qu’une fois
Symptôme : un effet sonore joue correctement la première fois, puis plus jamais.
Cause : après lecture, le status du ndspWaveBuf reste à « done ». Il faut réinitialiser le status et reflusher le cache avant de rejouer :
snd->wave_buf.status = NDSP_WBUF_FREE; // reinitialiser
DSP_FlushDataCache(snd->data, snd->size); // reflusher
ndspChnWaveBufAdd(channel, &snd->wave_buf); // rejouer
Oubli de romfsInit() : aucun asset ne charge
Symptôme : tous les fopen("romfs:/...") retournent NULL. Les polices, textures et sons ne chargent pas.
Cause : romfsInit() n’a pas été appelé au début du programme. Cet appel est obligatoire avant tout accès au système de fichiers RomFS.
Dimensions incorrectes des assets bannière CIA
Les dimensions sont strictes pour le format CIA :
- Icône : 48×48 pixels PNG exactement
- Bannière : 256×128 pixels PNG exactement
- Audio bannière : WAV PCM, de préférence court (~3 secondes)
Si les dimensions ne correspondent pas, bannertool échouera silencieusement ou produira un binaire corrompu qui fera planter l’installation.
14. Conclusion : créer son propre jeu homebrew pour Nintendo 3DS
Développer un jeu homebrew pour Nintendo 3DS, c’est un projet qui touche à plein de domaines passionnants de la programmation : programmation système embarquée, graphismes 2D avec citro2d, audio bas niveau avec NDSP, gestion mémoire spécifique, et cross-compilation ARM. Les contraintes de la plateforme (mémoire limitée, double écran, formats propriétaires) forcent à écrire du code propre et performant — et franchement, j’ai pris un plaisir fou à relever ces défis.
Les 6 règles d’or du développement homebrew 3DS
- Séparez votre logique de jeu du rendu. La logique portable dans un fichier indépendant, le rendu 3DS dans un autre. C’est la stratégie qui a permis de développer 2048-3DS efficacement.
- C2D_TargetClear avant chaque scène. Toujours. Sans exception. Sinon, artefacts VRAM.
- linearAlloc pour l’audio, jamais malloc. Et n’oubliez jamais
DSP_FlushDataCache(). - WAV PCM uniquement pour tous les fichiers audio. Convertissez vos MP3/OGG avant de les intégrer.
- Testez le .cia en plus du .3dsx. Les permissions sont différentes et un service oublié dans le RSF cause un crash.
- Déclarez tous les services système dans le fichier RSF, en particulier
dsp::DSP,csnd:SND,hid:USERetfs:USER.
Le projet 2048 pour Nintendo 3DS met en pratique chaque concept de ce tutoriel : rendu double écran, animations fluides, audio multi-pistes, sauvegarde binaire, localisation en 13 langues, système de succès, et packaging en .3dsx et .cia. Je l’ai mis en open source pour que ça serve de base à vos propres projets.
Télécharger 2048 pour Nintendo 3DS — disponible gratuitement en .3dsx et .cia. Jetez un oeil au code source pour voir tous ces concepts en action. Et si vous vous lancez dans un projet homebrew 3DS, n’hésitez pas à partager — la communauté est accueillante et toujours prête à filer un coup de main.
Ressources pour le développement homebrew 3DS
- devkitPro :
https://devkitpro.org/— Toolchain officielle pour le développement homebrew 3DS - Documentation libctru :
https://libctru.devkitpro.org/— Référence complète de l’API libctru - Headers citro2d : les fichiers headers dans
$DEVKITARM/../libctru/include/citro2d/sont la meilleure documentation de la bibliothèque graphique - 3dbrew Wiki :
https://www.3dbrew.org/— Wiki technique exhaustif de la Nintendo 3DS (hardware, formats, services système) - Exemples officiels :
https://github.com/devkitPro/3ds-examples— Exemples couvrant graphismes, audio et réseau - bannertool :
https://github.com/Steveice10/bannertool— Générateur de bannières et icônes pour le format CIA - makerom :
https://github.com/3DSGuy/Project_CTR— Assembleur de fichiers CIA/CCI pour Nintendo 3DS
Spécifications techniques de la Nintendo 3DS
- CPU : ARM11 MPCore (ARMv6K) @ 268 MHz, 2 cores (4 avec New 3DS)
- GPU : DMP PICA200 @ 268 MHz
- RAM : 128 MB (256 MB sur New 3DS), dont 6 MB VRAM dédiée
- Écran supérieur : 400×240 pixels (800×240 en mode 3D stéréoscopique)
- Écran inférieur : 320×240 pixels, tactile résistif
- Audio : 24 canaux DSP, sortie stéréo