Nintendo 3DS Homebrew entwickeln: dein eigenes Spiel in C

Nintendo 3DS Homebrew entwickeln: dein eigenes Spiel in C

Du willst dein eigenes Homebrew-Spiel für die Nintendo 3DS bauen? Gute Nachricht: Das geht nach wie vor, und es macht richtig Spaß. In diesem Tutorial teile ich alles, was ich beim Entwickeln von 2048 für Nintendo 3DS gelernt habe, ein komplettes Homebrew-Spiel mit Animationen, Audio, 13 Sprachen und Achievements, von der ersten Zeile Code bis zur fertigen installierbaren .cia-Datei.

Das fertige Ergebnis ansehen: 2048 für Nintendo 3DS, kostenlos als .3dsx und .cia zum Download.

Dieser Leitfaden deckt die komplette Pipeline der 3DS-Homebrew-Entwicklung ab: 2D-Grafik mit citro2d, Audio über NDSP, Touchscreen, das Dateisystem RomFS und das Packaging als CIA. Der gesamte Code ist C99. Keine graue Theorie, nur Code, der wirklich läuft, direkt aus dem Projekt 2048-3DS.

Inhalt

  1. Was ist ein 3DS-Homebrew
  2. Voraussetzungen: devkitARM und die 3DS-Werkzeuge installieren
  3. Aufbau eines Homebrew-Projekts: Spiellogik und Rendering trennen
  4. Die beiden Bildschirme der Nintendo 3DS ansteuern
  5. 2D-Grafik mit citro2d
  6. Eingaben verarbeiten: D-Pad, Circle Pad und Touchscreen
  7. 3DS-Homebrew-Audio: NDSP und das Format WAV PCM
  8. Dateisystem: RomFS und SD-Karte
  9. Daten auf der SD-Karte speichern
  10. Das 3DS-Makefile: ARM-Cross-Compilation mit devkitARM
  11. Eine .3dsx-Datei für den Homebrew Launcher erzeugen
  12. Eine installierbare .cia-Datei für das HOME-Menü bauen
  13. Typische Fallstricke und Fehler bei der 3DS-Homebrew-Entwicklung
  14. Fazit und Ressourcen zum Weiterlesen

1. Was ist ein Nintendo-3DS-Homebrew?

Ein 3DS-Homebrew ist eine inoffizielle Anwendung, die auf einer Nintendo 3DS mit Custom Firmware (CFW) wie Luma3DS läuft. Die 3DS-Homebrew-Szene erlaubt es, Spiele, Emulatoren und Werkzeuge für die Handheld-Konsole frei zu entwickeln und zu verteilen.

Die Nintendo 3DS hat einen ARM11-Prozessor (ARMv6K) mit 268 MHz und eine PICA200-GPU, dazu eine Besonderheit, die sie einzigartig macht: zwei Bildschirme. Der obere zeigt 400×240 Pixel (800×240 bei stereoskopischem 3D), der untere, berührungsempfindliche zeigt 320×240 Pixel.

Für 3DS-Homebrews gibt es zwei Verteilungsformate:

  • .3dsx: das Homebrew-Format, das über den Homebrew Launcher gestartet wird. Keine Installation, direkte Ausführung von der SD-Karte.
  • .cia: das installierbare Format, das im HOME-Menü der 3DS auftaucht, mit Icon und animiertem Banner. Setzt eine Custom Firmware (CFW) voraus.

Für 2048-3DS baue ich beide Formate, um möglichst viele Spieler zu erreichen. Die 3DS-Homebrew-Community lebt dank eines soliden Werkzeug-Ökosystems weiter: devkitARM (Toolchain), libctru (Systembibliothek), citro2d/citro3d (Grafik). Wer einen guten Spielplatz sucht, um hardwarenahe Systemprogrammierung oder Retro-Spieleentwicklung zu lernen, ist bei der 3DS genau richtig.

2. Voraussetzungen: devkitARM und die 3DS-Werkzeuge installieren

Bevor du anfängst, ein Spiel für die Nintendo 3DS zu programmieren, brauchst du die Toolchain für die ARM-Cross-Compilation und die konsolenspezifischen Bibliotheken.

Toolchain devkitARM: der Compiler für die Nintendo 3DS

devkitARM ist die GCC-Cross-Toolchain für ARM-Prozessoren, bereitgestellt vom Projekt devkitPro. Sie enthält Compiler, Linker und die Build-Werkzeuge, die du brauchst, um C-Code für die 3DS zu kompilieren. So installierst du sie:

bash
# Installation unter Linux/WSL (empfohlen für die 3DS-Entwicklung)
wget https://apt.devkitpro.org/install-devkitpro-pacman
chmod +x install-devkitpro-pacman
sudo ./install-devkitpro-pacman

# 3DS-Entwicklungspakete installieren
sudo dkp-pacman -S 3ds-dev

# Umgebungsvariable ist Pflicht
export DEVKITARM=/opt/devkitpro/devkitARM

Unverzichtbare Bibliotheken für die 3DS-Homebrew-Entwicklung

  • libctru: hardwarenahe C-Bibliothek für die Systemdienste der 3DS (HID, Dateisystem, Audio, GPU). Sie ist die Grundlage jedes 3DS-Homebrew-Programms.
  • citro3d: Abstraktion über der PICA200-GPU. Kümmert sich um Render-Targets, Framebuffer und Grafiksynchronisation.
  • citro2d: 2D-Schicht über citro3d. Liefert einfache Primitive (Rechtecke, Kreise, Ellipsen, Linien, Text, Sprites) und ist damit ideal für 2D-Spiele auf der 3DS.

Zusätzliche Werkzeuge für das Packaging

  • bannertool: erzeugt die Dateien banner.bin und icon.bin, die das CIA-Format braucht
  • makerom: setzt die fertige .cia-Datei aus ELF, RSF, Banner und Icon zusammen
  • tex3ds: wandelt PNG-Bilder in .t3x-Texturen um, die citro2d auf der PICA200-GPU lesen kann
  • mkbcfnt: wandelt TTF-Schriften in das .bcfnt-Format für die Textausgabe auf der 3DS um (unterstützt Unicode, CJK, Kyrillisch)

3. Aufbau eines 3DS-Homebrew-Projekts: Spiellogik und Rendering trennen

Der klassische Anfängerfehler in der Homebrew-Entwicklung ist, den gesamten Code direkt gegen die Konsolen-APIs zu schreiben. Das Ergebnis: Testen und Debuggen wird unmöglich. Ich bin da durch, und mein Rat an alle lautet: Trenne die Spiellogik vom Rendering, und zwar von Anfang an.

Genau diese Strategie steckt hinter 2048-3DS: Die Logik des Spiels (Raster, Bewegungen, Fusionen, Punktestand) ist zu 100 % portabel, das Rendering dagegen plattformspezifisch.

Dateistruktur des Projekts 2048-3DS

treeview
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)

Portabler Code: die 2048-Spiellogik ohne Abhängigkeiten

Die Datei logic.c von 2048-3DS bindet keinen einzigen Header der Nintendo 3DS ein. Sie nutzt ausschließlich Standard-C-Header (<stdint.h>, <stdlib.h>, <string.h>, <time.h>). Die Datei main_3ds.c ruft die öffentlichen Funktionen der Logik auf:

c
// logic.h — portable Schnittstelle des Spiels 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 vor der Bewegung
    int to_r, to_c;       // Position nach der Bewegung
    uint16_t value;       // während der Animation angezeigter Wert
    int merged;           // 1 bei Fusion
} TileAnim;

typedef struct {
    uint16_t cells[GRID_SIZE][GRID_SIZE];  // 4x4-Raster
    uint32_t score;
    int won, over;
    TileAnim anims[MAX_TILE_ANIMS];  // Animationsdaten des letzten Zugs
    int anim_count;
    int spawn_r, spawn_c;     // Position der neuen Kachel
    uint16_t spawn_val;        // Wert (2 zu 90 %, 4 zu 10 %)
} Game;

void game_init(Game *g);                    // leeres Raster + 2 Kacheln
int  game_move(Game *g, Direction dir);     // gibt 1 zurück, wenn sich das Raster geändert hat
int  game_is_over(Game *g);                 // keine Bewegung mehr möglich
int  game_has_won(Game *g);                 // Kachel >= 2048 erreicht

Das 3DS-Makefile kompiliert logic.c + achievements.c + main_3ds.c und filtert main_sdl.c explizit heraus. Die Spiellogik bleibt gleich, egal welches Render-Frontend darüberliegt.

4. Die beiden Bildschirme der Nintendo 3DS ansteuern

Was die Nintendo 3DS von jedem anderen Handheld unterscheidet, sind ihre zwei Bildschirme. Sie sinnvoll zu nutzen, ist der Schlüssel zu einem guten Homebrew-Erlebnis.

  • Oberer Bildschirm (top): 400×240 Pixel, nicht berührungsempfindlich. Ideal für das eigentliche Gameplay. In 2048-3DS zeige ich hier das 4×4-Raster mit den Kachelanimationen.
  • Unterer Bildschirm (bottom): 320×240 Pixel, berührungsempfindlich. Ideal für Oberfläche, Menüs und Steuerung. In 2048-3DS liegen dort Punktestand, Buttons, Einstellungen und Achievements.

Render-Targets mit citro2d initialisieren

c
// Initialisierung des 3DS-Grafiksystems
gfxInitDefault();
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
C2D_Prepare();

// Für jeden Bildschirm ein Render-Target anlegen
C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);

Renderschleife für zwei Bildschirme

In jedem Frame musst du auf beide Bildschirme getrennt zeichnen. So ist die Renderschleife von 2048-3DS aufgebaut:

c
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);

// Oberer Bildschirm — Raster des Spiels 2048
C2D_TargetClear(top, couleur_fond);
C2D_SceneBegin(top);
render_top_screen(&game, anim_phase, progress);

// Unterer Bildschirm — UI (Punkte, Buttons, Einstellungen)
C2D_TargetClear(bot, col_bot_bg);
C2D_SceneBegin(bot);
render_bot_game(&game, best_score, 0, music_muted);

C3D_FrameEnd(0);

Kritischer Fallstrick beim 3DS-Rendering: C2D_TargetClear ist Pflicht. Ohne C2D_TargetClear() vor jeder Szene bekommst du zufällige Bildfehler, VRAM-Reste des vorherigen Frames. Der VRAM der 3DS wird nicht automatisch initialisiert. Dieser Bug ist für Homebrew-Einsteiger extrem verwirrend, weil die Artefakte in jedem Frame anders aussehen.

Bildschirmmaße: die wichtigsten Konstanten

c
#define TOP_W  400   // Breite oberer Bildschirm
#define TOP_H  240   // Höhe oberer Bildschirm
#define BOT_W  320   // Breite unterer Bildschirm (Touch)
#define BOT_H  240   // Höhe unterer Bildschirm

Achtung: Auf der echten Konsole sitzt der untere Bildschirm horizontal zentriert unter dem oberen (400 gegenüber 320 Pixel Breite). Die Touch-Koordinaten entsprechen direkt den Pixeln des unteren Bildschirms, mit (0,0) oben links.

5. 2D-Grafik mit citro2d auf der Nintendo 3DS

citro2d ist die Standardbibliothek für 2D-Rendering in 3DS-Homebrew-Spielen. Sie setzt auf citro3d und der PICA200-GPU auf und übernimmt intern das Batching der Primitive sowie die Verwaltung der GPU-Texturen. Hier ist der praktische Einsatz, mit konkreten Beispielen aus 2048-3DS.

Primitive zeichnen: Rechtecke, Kreise, Ellipsen und Linien

c
// Gefülltes Rechteck (Kacheln des Spiels 2048)
C2D_DrawRectSolid(x, y, z, largeur, hauteur, couleur);

// Gefüllter Kreis (abgerundete Ecken, Icons)
C2D_DrawCircleSolid(centre_x, centre_y, z, rayon, couleur);

// Gefüllte Ellipse (Musik-Icon in 2048-3DS)
C2D_DrawEllipseSolid(x, y, z, largeur, hauteur, couleur);

// Linie (Balken für die Stummschaltung)
C2D_DrawLine(x1, y1, couleur1, x2, y2, couleur2, epaisseur, z);

Der Parameter z steuert die Tiefe (0.0f für die Standardebene). Farben liegen auf der 3DS im Speicher im Format ABGR vor, Vorsicht, das ist die Umkehrung des klassischen ARGB. citro2d liefert das Makro C2D_Color32(r, g, b, a), um Farben korrekt zusammenzubauen:

c
// C99-Falle: C2D_Color32() ist nicht constexpr
// Für globale Farbkonstanten ein Makro verwenden
#define MAKE_COLOR(r,g,b,a) \
    ((u32)(r) | ((u32)(g)<<8) | ((u32)(b)<<16) | ((u32)(a)<<24))

// Farbpalette des Spiels 2048 auf der 3DS
#define col_grid_bg  MAKE_COLOR(0xBB, 0xAD, 0xA0, 0xFF)  // Hintergrund Raster
#define col_bot_bg   MAKE_COLOR(0xFA, 0xF8, 0xEF, 0xFF)  // Hintergrund unterer Bildschirm
#define col_text_dk  MAKE_COLOR(0x77, 0x6E, 0x65, 0xFF)  // dunkler Text
#define col_text_lt  MAKE_COLOR(0xF9, 0xF6, 0xF2, 0xFF)  // heller Text

Abgerundete Rechtecke: ein Grafiktrick für citro2d

citro2d kennt keine Primitive für ein „abgerundetes Rechteck“. Für die Kacheln des 2048-Spiels und die Buttons der Oberfläche habe ich diese Technik gebaut: ein mittleres Rechteck, zwei seitliche Rechtecke und vier Kreise an den Ecken:

c
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;
    }
    // Mittlerer Korpus + seitliche Ränder
    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 Kreise mit Radius r an den Ecken
    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);
}

Diese Funktion kommt in 2048-3DS überall zum Einsatz: Spielkacheln, Punkteboxen, Buttons, Lautstärkeregler, Bestätigungsdialoge.

Text auf der Nintendo 3DS ausgeben: das Format .bcfnt

Die Textausgabe auf der 3DS läuft über das Format .bcfnt (Binary CTR Font). Jede Zeichenkette muss erst in einen Textbuffer „geparst“ werden, bevor sie gezeichnet werden kann. In 2048-3DS nutze ich das für Punktestände, Button-Beschriftungen in 13 Sprachen und Achievement-Benachrichtigungen:

c
// Initialisierung der Textbuffer
C2D_TextBuf s_dynamicBuf = C2D_TextBufNew(512);
C2D_Font s_font = C2D_FontLoad("romfs:/font.bcfnt");

// Hilfsfunktion: zentrierten Text zeichnen
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);
}

// Wichtig: Textbuffer in jedem Frame leeren
C2D_TextBufClear(s_dynamicBuf);

Der Faktor 0.62f ist so gewählt, dass der Text optisch sauber vertikal zentriert sitzt.

Sprites und GPU-Texturen im Format .t3x

Bilder müssen mit dem Werkzeug tex3ds ins Format .t3x umgewandelt werden (3DS-Textur, optimiert für die PICA200-GPU). Eine .t3s-Datei beschreibt das Sprite Sheet. In 2048-3DS stecken die 8 Achievement-Icons (je 32×32 Pixel) in einem einzigen Sprite Sheet:

code
# 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)
c
// Sprite Sheet aus RomFS laden
C2D_SpriteSheet sheet = C2D_SpriteSheetLoad("romfs:/sprites.t3x");

// Ein Achievement-Icon zeichnen
C2D_Image img = C2D_SpriteSheetGetImage(sheet, index);
C2D_DrawImageAt(img, x, y, 0.0f, NULL, scale_x, scale_y);

// Darstellung in Graustufen (Achievement gesperrt)
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);

Flüssige Animationen auf der Nintendo 3DS

Für flüssige Animationen in einem 3DS-Homebrew-Spiel nimmst du osGetTime() (Zeit in Millisekunden) und interpolierst mit Easing. Das ist das Animationssystem, das in 2048-3DS für das Gleiten und Erscheinen der Kacheln sorgt:

c
// Timing der Animationen im Spiel 2048
#define ANIM_SLIDE_MS  120    // Dauer des Kachelgleitens
#define ANIM_POP_MS    100    // Dauer des „Pop“ (Fusion/Erscheinen)

typedef enum { ANIM_NONE, ANIM_SLIDING, ANIM_POPPING } AnimPhase;

// Quadratisches Easing: natürliches Abbremsen
static float ease_out_quad(float t) {
    return t * (2.0f - t);
}

// In der Hauptschleife: Positionen interpolieren
u64 now = osGetTime();
u64 elapsed = now - anim_start;
float progress = (float)elapsed / ANIM_SLIDE_MS;
float t = ease_out_quad(progress);

// Lineares Gleiten der Kachel
float cur_x = from_px + (to_px - from_px) * t;
float cur_y = from_py + (to_py - from_py) * t;

Die Animation läuft in zwei Phasen: zuerst das Gleiten (120 ms), dann der „Pop“ fusionierte Kacheln wachsen auf 125 % an und gehen wieder auf 100 % zurück, während die neue Kachel von 0 auf 100 % aufgeht. Dieses System macht das 2048-Spiel auf der 3DS visuell genauso befriedigend wie das Original.

6. Eingaben auf der Nintendo 3DS: D-Pad, Circle Pad und Touchscreen

Die Nintendo 3DS bietet Homebrew-Spielen mehrere Eingabegeräte, alle über die Bibliothek libctru (HID) erreichbar. In 2048-3DS nutze ich alle drei: D-Pad und Circle Pad zum Verschieben der Kacheln, den Touchscreen für Buttons und Regler.

Physische Tasten lesen: D-Pad und die Tasten A/B/X/Y

c
hidScanInput();            // Eingabestatus einlesen (1x pro Frame)
u32 kDown = hidKeysDown(); // In diesem Frame neu gedrückte Tasten

// D-Pad: Kacheln im Spiel 2048 verschieben
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; }

// Systemtasten
if (kDown & KEY_B)      { /* Zurück-Navigation */ }
if (kDown & KEY_START)  { break; /* Anwendung beenden */ }
if (kDown & KEY_SELECT) { game_init(&game); /* neues Spiel */ }

Circle Pad: der Analogstick der 3DS mit Deadzone

c
circlePosition cpad;
hidCircleRead(&cpad);

// Deadzone von 80 gegen Fehlauslösungen
// Bereich des Circle Pad: etwa -155 bis +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; }

Die Deadzone ist beim Circle Pad der 3DS entscheidend. Ohne Schwellwert meldet der Stick permanent Mikrobewegungen und löst ungewollte Züge aus. Ein Wert von 80 (bei einem Bereich von etwa -155 bis +155) funktioniert für ein Spiel wie 2048 in der Praxis gut.

Touchscreen: Klicks auf die Buttons der Oberfläche auswerten

Der Touchscreen der 3DS ist resistiv (druckbasiert, nicht kapazitiv). Die Koordinaten liegen direkt in Pixeln des unteren Bildschirms (320×240). In 2048-3DS bedient der Touchscreen die Buttons „Neues Spiel“, „Achievements“ und „Einstellungen“, die Lautstärkeregler und die Sprachauswahl:

c
if (kDown & KEY_TOUCH) {
    touchPosition touch;
    hidTouchRead(&touch);
    int mx = touch.px;  // 0..319
    int my = touch.py;  // 0..239

// Klick auf den Button „Neues Spiel“ erkennen
    float btn_x = (BOT_W - BTN_W) / 2;  // horizontal zentriert
    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;
    }
}

Nimm hidKeysDown() mit KEY_TOUCH für einzelne Tipper (Buttons) und hidKeysHeld() für fortlaufende Interaktionen (Lautstärkeregler, Ziehen).

Hauptschleife eines 3DS-Homebrew-Spiels

c
while (aptMainLoop()) {
    u64 now = osGetTime();
    audio_music_tick(music_volume, music_muted);

hidScanInput();
    u32 kDown = hidKeysDown();

if (kDown & KEY_START) break;  // Sauberes Beenden

// Eingaben verarbeiten (D-Pad, Circle Pad, Touch)
    // Spiellogik von 2048 aktualisieren
    // Beide Bildschirme rendern
    // ...
}

aptMainLoop() kümmert sich um den Lebenszyklus der Homebrew-Anwendung: Ruhezustand, Beenden durch das System, Rückkehr ins HOME-Menü.

7. 3DS-Homebrew-Audio: NDSP und das Format WAV PCM

Glaub mir aus Erfahrung: Audio auf der Nintendo 3DS ist der Bereich, in dem ich die meiste Zeit verloren habe. Auf dem Papier sieht alles einfach aus, in der Praxis lauern überall Fallen. Das Audio-Backend der 3DS heißt NDSP (Nintendo DSP): Es braucht die DSP-Firmware (dspfirm.bin auf der SD-Karte) und beherrscht Mehrkanal-Mixing mit linearer Interpolation.

In 2048-3DS habe ich Hintergrundmusik auf 4 Tracks sowie Soundeffekte für Kachelbewegungen und Achievements umgesetzt.

Audioformat für die 3DS: WAV PCM ist Pflicht

Der größte Fallstrick beim 3DS-Audio: Die Konsole unterstützt ausschließlich rohes WAV PCM (kein MP3, kein OGG, kein ADPCM-komprimiertes WAV). Die Dateien müssen 8 oder 16 Bit PCM sein, mono oder stereo. Jedes andere Format wird stillschweigend ignoriert oder führt zum Absturz.

Speicher für 3DS-Audio: linearAlloc statt malloc

Audiobuffer auf der 3DS müssen mit linearAlloc() belegt werden, nicht mit malloc(). Auf linearen Speicher greift der DSP-Prozessor direkt zu, auf den normalen Heap nicht.

c
// Struktur zum Speichern einer WAV-Audiodatei auf der 3DS
typedef struct {
    u8  *data;            // PCM-Daten (mit linearAlloc belegt)
    u32  size;            // Größe in Bytes
    u32  sample_rate;     // Abtastrate
    u16  channels;        // 1 (Mono) oder 2 (Stereo)
    u16  bits_per_sample; // 8 oder 16 Bit
    ndspWaveBuf wave_buf; // NDSP-Buffer
    int  loaded;          // Flag: Laden erfolgreich
} WavSound;

WAV-Dateien im 3DS-Homebrew laden

c
static int wav_load(WavSound *snd, const char *path)
{
    FILE *f = fopen(path, "rb");
    if (!f) return -1;

// RIFF/WAVE-Header lesen und prüfen
    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;
    }

// WAV-Chunks durchlaufen (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) {
            // channels, sample_rate, bits_per_sample auslesen
            // ...
        } else if (memcmp(chunk_id, "data", 4) == 0) {
            // WICHTIG: linearAlloc, nicht malloc!
            snd->data = (u8 *)linearAlloc(chunk_size);
            fread(snd->data, 1, chunk_size, f);
        } else {
            fseek(f, chunk_size, SEEK_CUR);
        }
    }

// NDSP-Buffer vorbereiten
    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;

// PFLICHT: CPU-Cache zum DSP flushen
    DSP_FlushDataCache(snd->data, snd->size);
    return 0;
}

DSP_FlushDataCache ist Pflicht nach jedem Schreibzugriff auf einen Audiobuffer der 3DS. Ohne diesen Aufruf liest der DSP kaputte Daten, weil der CPU-Cache nicht mit dem DSP-Speicher kohärent ist. Das ist die Ursache Nummer 1 für stumme Audio-Bugs auf der Nintendo 3DS.

NDSP-Audio initialisieren

So sieht die Audio-Initialisierung in 2048-3DS aus:

c
static void audio_init(void)
{
    if (ndspInit() != 0) return;

ndspSetOutputMode(NDSP_OUTPUT_STEREO);

// Kanal 0: Hintergrundmusik
    // Kanal 1: SFX (Kachelbewegung)
    // Kanal 2: SFX (Achievement freigeschaltet)
    for (int ch = 0; ch < 3; ch++) {
        ndspChnReset(ch);
        ndspChnSetInterp(ch, NDSP_INTERP_LINEAR);
        ndspChnSetFormat(ch, NDSP_FORMAT_STEREO_PCM16);
    }
}

Einen Sound abspielen und die Lautstärke steuern

c
// NDSP: Audio-Buffer zum Kanal hinzufügen
snd->wave_buf.status = NDSP_WBUF_FREE;  // Status zurücksetzen
DSP_FlushDataCache(snd->data, snd->size);
ndspChnWaveBufAdd(channel, &snd->wave_buf);

// NDSP-Lautstärke: Array aus 12 Floats (Stereo-Mix pro Kanal)
float vol = (float)music_volume / 128.0f;  // 0..128 -> 0.0..1.0
float mix[12] = {0};
mix[0] = vol;  // linker Kanal
mix[1] = vol;  // rechter Kanal
ndspChnSetMix(0, mix);

Musiktitel automatisch aneinanderhängen

In 2048-3DS laufen 4 Musiktitel automatisch hintereinander (intro, music1, music2, music3). In jedem Frame prüfe ich, ob der aktuelle Titel zu Ende ist:

c
static void audio_music_tick(int music_volume, int music_muted)
{
    if (!s_audio_init || music_muted) return;

if (!ndspChnIsPlaying(0)) {
        // Zum nächsten Titel wechseln (zyklische Schleife)
        s_music_current = (s_music_current + 1) % MUSIC_TRACK_COUNT;
        audio_play_current_track(music_volume, music_muted);
    }
}

Audio am Programmende aufräumen

c
// Audio-Ressourcen sauber freigeben
ndspChnReset(0);
ndspChnReset(1);
ndspChnReset(2);
ndspExit();

// Linearen Speicher freigeben (nicht free, sondern 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. Das Dateisystem der 3DS: RomFS und SD-Karte

Die Nintendo 3DS stellt Homebrew-Anwendungen zwei Dateisysteme bereit: RomFS für Assets, die nur gelesen werden, und SDMC für Lesen und Schreiben auf der SD-Karte.

RomFS: Assets direkt ins Homebrew-Binary packen

Mit RomFS (Read-Only Memory FileSystem) packst du Dateien direkt in die .3dsx- oder .cia-Datei. Ideal für Assets, die sich nie ändern: Schriften, Texturen, Musik, Sprites. In 2048-3DS enthält der Ordner romfs/ die .bcfnt-Schrift mit CJK-Unterstützung, das Sprite Sheet der Achievements und 6 WAV-Dateien:

c
// Pflicht-Initialisierung vor jedem RomFS-Zugriff
romfsInit();

// Zugriff auf die Assets mit dem Präfix 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");

Der Inhalt des Ordners romfs/ wird vom 3DS-Makefile automatisch eingebettet:

bash
# In Makefile.3ds
ROMFS := romfs

Asset-Formate speziell für die Nintendo 3DS

  • .bcfnt: Bitmap-Schrift, kompiliert mit mkbcfnt. Unterstützt vollständiges Unicode (CJK, Kyrillisch, Akzentzeichen). In 2048-3DS deckt eine einzige Schrift alle 13 Sprachen ab.
  • .t3x: GPU-Textur, kompiliert mit tex3ds. Für die PICA200 optimiertes Format. Landet ohne Umwandlung direkt im VRAM.
  • .wav: rohes PCM-Audio. Keine Umwandlung zur Laufzeit, muss aber unkomprimiertes PCM sein (siehe Audio-Abschnitt).

SDMC: Daten auf der SD-Karte der 3DS speichern

Zum Speichern von Daten (Punktestände, Einstellungen, Fortschritt) schreiben 3DS-Homebrews mit dem Präfix sdmc:/ auf die SD-Karte:

c
// Speicherpfade für 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"

// Speicherverzeichnis beim Start anlegen
#include <sys/stat.h>
mkdir("sdmc:/3ds", 0777);
mkdir(SAVE_DIR, 0777);

Geschrieben wird mit den Standard-C-Funktionen (fopen, fwrite, fclose). Für Dateioperationen auf SDMC brauchst du keine 3DS-spezifische API.

9. Daten im 3DS-Homebrew auf der SD-Karte speichern

Für ein 3DS-Homebrew-Spiel ist ein binärer Speicherstand der direkteste und sparsamste Weg. So löst 2048-3DS die Persistenz von Achievements und Einstellungen.

Binärer Speicherstand für die Achievements

Das Spiel 2048-3DS hat 8 Achievements (Punkteschwellen: 500, 1000, 2500, 5000, 10000, 20000, 50000, 100000 Punkte). Der Speicherstand belegt 8 Byte, ein Byte pro Achievement:

c
// Speicherstand: 8 Bytes (0x00 = gesperrt, 0x01 = freigeschaltet)
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;
}

// Laden mit Standardwerten, falls die Datei fehlt
int achievements_load(Achievements *a, const char *path)
{
    achievements_init(a);  // standardmäßig alle gesperrt
    FILE *f = fopen(path, "rb");
    if (!f) return -1;     // erster Start: keine Datei
    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;
}

Benutzereinstellungen speichern

Die Einstellungen von 2048-3DS (Sprache, Musiklautstärke, Effektlautstärke, Stummschaltung) landen in 4 Byte:

c
// Format: [0] = Sprache, [1] = Musiklautstärke, [2] = SFX-Lautstärke, [3] = Stumm
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);
}

Die Vorteile für ein Homebrew-Spiel: feste Größe, kein Parsen, keine Abhängigkeit von einer JSON- oder XML-Bibliothek, sofortiges Lesen. Gespeichert wird automatisch, sobald der Nutzer das Einstellungsmenü oder die Anwendung verlässt.

10. Das 3DS-Makefile: ARM-Cross-Compilation mit devkitARM

Das Build-System für 3DS-Homebrews baut auf den Regeln von devkitARM auf. Wer das 3DS-Makefile versteht, debuggt Compile-Probleme deutlich schneller.

Aufbau des Makefiles für ein 3DS-Homebrew-Projekt

bash
# Prüfen, ob devkitARM konfiguriert ist
ifeq ($(strip $(DEVKITARM)),)
$(error "Please set DEVKITARM in your environment")
endif

include $(DEVKITARM)/3ds_rules

# Konfiguration des Homebrew-Projekts
TARGET   := 2048
BUILD    := build_3ds
SOURCES  := source
ROMFS    := romfs

# Metadaten für den Homebrew Launcher und das HOME-Menü
APP_TITLE       := 2048
APP_DESCRIPTION := 2048 puzzle game for 3DS
APP_AUTHOR      := Gekkode

Compiler-Flags für ARM auf der Nintendo 3DS

makefile
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__

Was die einzelnen Flags der ARM-Cross-Compilation für die 3DS bedeuten:

  • -march=armv6k: Architektur des ARM11-Prozessors der Nintendo 3DS
  • -mtune=mpcore: optimiert den Code für den MPCore
  • -mfloat-abi=hard: nutzt die VFP (Fließkomma in Hardware), entscheidend für die Grafikleistung
  • -mword-relocations: erzeugt die Relokationen, die das 3DSX-Format braucht
  • -ffunction-sections: erlaubt dem Linker, toten Code zu entfernen und das Binary zu verkleinern
  • -D__3DS__: setzt das Makro für bedingte #ifdef-Blöcke zwischen PC und 3DS

citro2d und libctru linken

makefile
LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS    := -lcitro2d -lcitro3d -lctru -lm

Die Reihenfolge der Bibliotheken zählt für den GNU-Linker: -lcitro2d hängt von -lcitro3d ab, das wiederum von -lctru. Immer von der höchsten zur niedrigsten Abstraktionsebene sortieren.

Quelldateien filtern: das PC-Rendering aus dem 3DS-Build nehmen

bash
# main_sdl.c vom 3DS-Build ausschließen (nur main_3ds.c wird kompiliert)
CFILES := $(filter-out main_sdl.c, \
    $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))))

Dieser Filter stellt sicher, dass für den 3DS-Build nur main_3ds.c kompiliert wird.

Build-Befehle

bash
# Das Homebrew als .3dsx kompilieren
make -f Makefile.3ds

# Kompilieren und eine installierbare .cia erzeugen
make -f Makefile.3ds cia

# Vollständiges Aufräumen
make -f Makefile.3ds clean

11. Eine .3dsx-Datei für den Homebrew Launcher erzeugen

Das Format .3dsx ist das Standard-Homebrew-Format der Nintendo 3DS und wird über den Homebrew Launcher gestartet. Es ist am einfachsten zu verteilen: eine einzige Datei, die auf die SD-Karte kopiert wird.

Das 3DS-Makefile erzeugt automatisch zwei Dateien:

  • 2048.3dsx: die ausführbare Homebrew-Datei, inklusive eingebettetem RomFS
  • 2048.smdh: die SMDH-Metadaten (Titel, Autor, Beschreibung, Icon)

SMDH: die Metadaten deines 3DS-Homebrews

Die SMDH-Datei (Simple Metadata Header) enthält die Informationen, die der Homebrew Launcher anzeigt:

bash
# In Makefile.3ds definiert
APP_TITLE       := 2048
APP_DESCRIPTION := 2048 puzzle game for 3DS
APP_AUTHOR      := Gekkode

Das Icon ist ein PNG mit 48×48 Pixeln und wird automatisch erkannt, wenn icon.png oder 2048.png im Projektstamm liegt.

Das RomFS in die .3dsx einbetten

Das RomFS steckt direkt in der .3dsx-Datei. Der Endnutzer hat damit nur eine einzige Datei zu handhaben:

c
_3DSXFLAGS += --romfs=$(CURDIR)/romfs

12. Eine installierbare .cia-Datei für das HOME-Menü der 3DS bauen

Das Format .cia (CTR Importable Archive) ist das installierbare Format der Nintendo 3DS. Einmal über FBI oder einen anderen Title-Manager installiert, erscheint das Spiel im HOME-Menü der Konsole, mit eigenem Icon, animiertem Banner und Startsound.

In diesem Format verteile ich 2048 für Nintendo 3DS als .cia.

Achtung: Das ist der mit Abstand heikelste Schritt der 3DS-Homebrew-Entwicklung. Ich habe ewig gebraucht, um zu verstehen, warum eine .cia im Emulator Citra tadellos läuft und auf einer echten 3DS sofort abstürzt. Die Ursache ist fast immer eine falsch konfigurierte RSF-Datei. Was jetzt kommt, ist das Ergebnis vieler Teststunden auf echter Hardware, ich erkläre jeden Parameter, damit du dir das ersparst.

Assets, die eine .cia braucht

  • Icon: PNG mit exakt 48×48 Pixeln (wird im HOME-Menü angezeigt)
  • Bannerbild: PNG mit exakt 256×128 Pixeln (erscheint oben auf dem Bildschirm, wenn das Spiel ausgewählt ist)
  • Bannersound: WAV, PCM 16 Bit, 44100 Hz, stereo, etwa 3 Sekunden lang (läuft, wenn das Spiel im HOME-Menü ausgewählt wird). Das Format ist strikt, konvertiere mit ffmpeg:
bash
# Beliebiges Audio in bannertool-taugliches WAV umwandeln
ffmpeg -i music.mp3 -acodec pcm_s16le -ar 44100 -ac 2 -t 3 banner.wav

So entsteht eine CIA-Datei für die 3DS

Der Bau einer .cia läuft in 4 Schritten ab. Empfehlenswert ist ein RSF-Template mit Variablen $(VARIABLE), die makerom über die Flags -DVARIABLE="valeur" ersetzt. So trennst du die Konfiguration (im Makefile) vom Berechtigungs-Template (im RSF), und der Build bleibt sauber und wiederverwendbar:

bash
# 1. ELF-Binary bauen
make -f Makefile.3ds

# 2. Banner erzeugen (Bild + Startsound)
bannertool makebanner \
    -i assets/banner.png \
    -a assets/music/banner.wav \
    -o build_3ds/banner.bnr

# 3. SMDH-Icon für das HOME-Menü erzeugen
bannertool makesmdh \
    -s "2048" \
    -l "2048 - puzzle game for 3DS" \
    -p "Gekkode" \
    -i icon.png \
    -o build_3ds/icon.icn

# 4. Fertige .cia mit makerom zusammenbauen
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"

Jedes Flag -DXXX="valeur" ersetzt die passende Variable $(XXX) in der RSF-Datei. Dank dieser Substitution reicht ein einziges wiederverwendbares RSF-Template für all deine Projekte, von Projekt zu Projekt ändern sich nur die -D-Flags.

Die wichtigen makerom-Flags:

  • -target t: Ziel „test“ (für Homebrews, die Nintendo nicht signiert hat)
  • -exefslogo: nimmt das Logo ins ExeFS auf (nötig für den Splashscreen beim Start)
  • -major / -minor / -micro: Version des Titels (wird in den Systemeinstellungen angezeigt)

Die RSF-Datei: die Berechtigungen deines CIA-Homebrews festlegen

Die RSF-Datei (ROM Specification File) ist der kritischste Teil des CIA-Builds. Sie definiert Metadaten, Systemberechtigungen, erlaubte Dienste und verfügbare Systemaufrufe. Eine unvollständige RSF bedeutet sofortigen Absturz auf echter Hardware, auch wenn in Citra alles läuft.

Hier ist das vollständige RSF-Template von 2048-3DS, entstanden nach vielen Tests auf echter Hardware. Jeder Abschnitt wird danach erklärt:

BasicInfo: Identität des Titels

yaml
BasicInfo:
  Title                   : $(APP_TITLE)
  ProductCode             : $(APP_PRODUCT_CODE)
  Logo                    : Nintendo
  • Title: der Name, der in den Systemeinstellungen erscheint. Nutzt die Variable $(APP_TITLE), ersetzt durch -DAPP_TITLE="2048".
  • ProductCode: eine Kennung im Format CTR-H-XXXX. Das H steht für Homebrew. Wähle einen eindeutigen Code (z. B. CTR-H-2048).
  • Logo: der animierte Splashscreen beim Start.
    • Nintendo: das offizielle animierte 3DS-Logo (empfohlen, das ist das Standardverhalten)
    • Homebrew: das Logo der Homebrew-Community
    • Licensed / Distributed: Varianten des Nintendo-Logos
    • None: besser meiden, kann auf manchen Firmware-Versionen Abstürze auslösen

RomFs: eingebettete Ressourcen

c
RomFs:
  RootPath: $(APP_ROMFS)

Pfad zum Ordner romfs/ mit den eingebetteten Assets (Sprites, Schriften, Audio). Wird über -DAPP_ROMFS="romfs" übergeben. Hat deine Anwendung kein RomFS, kannst du diesen Abschnitt weglassen.

TitleInfo: eindeutige Kennung

yaml
TitleInfo:
  Category                : $(APP_CATEGORY)
  UniqueId                : $(APP_UNIQUE_ID)
  • Category: für ein Homebrew-Spiel oder -Werkzeug immer Application.
  • UniqueId: eindeutige Hexadezimalkennung im Bereich 0xF0000 bis 0xFFFFF (der für Homebrews reservierte Bereich). Jedes installierte Homebrew braucht eine andere UniqueId, sonst gibt es Konflikte. Für 2048 nehme ich 0xF2048.

Option: Packaging-Optionen

c
Option:
  UseOnSD                 : $(APP_USE_ON_SD)
  FreeProductCode         : true
  MediaFootPadding        : false
  EnableCrypt             : $(APP_ENCRYPTED)
  EnableCompress          : true
  • UseOnSD: true (Pflicht für ein Homebrew, das auf der SD-Karte installiert wird).
  • FreeProductCode: true, damit makerom einen freien ProductCode akzeptiert (ohne Prüfung des Nintendo-Formats).
  • EnableCrypt: false (Homebrews haben keine Nintendo-Schlüssel zum Verschlüsseln).
  • EnableCompress: true, um den .code-Abschnitt des ExeFS zu komprimieren (verkleinert die .cia).

AccessControlInfo: der kritische Abschnitt

Hier entscheiden sich 90 % aller CIA-Abstürze. Dieser Abschnitt legt fest, was deine Anwendung auf der Konsole überhaupt darf. Fehlt auch nur ein Systemaufruf oder ein Dienst, stürzt die CIA auf echter Hardware sofort ab, während Citra diese Einschränkungen schlicht ignoriert.

yaml
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

Die Parameter, die du an dein Projekt anpassen solltest:

  • MemoryType: Application für ein normales Spiel oder Werkzeug. System für ein Systemmodul.
  • SystemMode: Menge an zugewiesenem RAM. 64MB ist der Standard. 96MB nur für Anwendungen, die exklusiv auf der New 3DS laufen und mehr Speicher brauchen.
  • SystemModeExt: Legacy für Kompatibilität mit Old 3DS und New 3DS.
  • CpuSpeed: 268MHz aktiviert den Taktboost auf der New 3DS (auf der Old 3DS fällt der Wert automatisch auf 268 MHz zurück).
  • EnableL2Cache: standardmäßig false. Auf true setzen für CPU-intensive Anwendungen auf der New 3DS (kann auf der Old 3DS instabil werden).

Danach folgen die vier Unterabschnitte für Berechtigungen. Mein Rat: Nimm das komplette Set. Ein einfaches Homebrew nutzt nicht alle SVCs und Dienste, aber zu viele zu deklarieren kostet auf einer Konsole mit CFW weder Leistung noch Sicherheit, einen einzigen zu vergessen führt dagegen zum sofortigen Absturz:

FileSystemAccess: Zugriff auf das Dateisystem

yaml
  FileSystemAccess:
   - CategorySystemApplication
   - CategoryHardwareCheck
   - CategoryFileSystemTool
   - Debug
   - TwlCardBackup
   - TwlNandData
   - Boss
   - DirectSdmc
   - Core
   - CtrNandRo
   - CtrNandRw
   - CtrNandRoWrite
   - CategorySystemSettings
   - CardBoard
   - ExportImportIvs
   - DirectSdmcWrite
   - SwitchCleanup
   - SaveDataMove
   - Shop
   - Shell
   - CategoryHomeMenu
   - SeedDB

Für ein einfaches Homebrew zählen vor allem DirectSdmc + DirectSdmcWrite (Lesen und Schreiben auf der SD) sowie Core. Sicherer ist es trotzdem, das komplette Set aufzunehmen.

IoAccessControl: hardwarenaher I/O-Zugriff

yaml
  IoAccessControl:
   - FsMountNand
   - FsMountNandRoWrite
   - FsMountTwln
   - FsMountWnand
   - FsMountCardSpi
   - UseSdif3
   - CreateSeed
   - UseCardSpi

SystemCallAccess: ARM11-Systemaufrufe (SVCs)

Diese Liste legt fest, welche Systemaufrufe (Supervisor Calls) deine Anwendung nutzen darf. Das ist der kritischste Abschnitt: Fehlt ein SVC, den libctru aufruft, stürzt die Konsole sofort ab. Nimm alle SVCs von 1 bis 125 auf:

yaml
  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

Wichtig: Die Nummern sind nicht durchgehend, es gibt Lücken (63-70, 89-95, 110-111, 116). Das ist normal, diese SVCs sind reserviert oder vom 3DS-Kernel nicht implementiert.

ServiceAccessControl: Systemdienste

Die Dienste sind die High-Level-APIs der 3DS. Jede libctru-Bibliothek nutzt einen oder mehrere davon. Ist ein Dienst hier nicht deklariert, schlägt der Aufruf von srvGetServiceHandle() fehl und die Initialisierung der zugehörigen Bibliothek stürzt ab:

yaml
  ServiceAccessControl:
   - APT:U          # Lebenszyklus der Anwendung (aptMainLoop)
   - ac:u           # Netzwerkkonfiguration
   - am:net         # Application Manager (Titel installieren)
   - boss:U         # SpotPass
   - cam:u          # Kamera
   - cecd:u         # StreetPass
   - cfg:nor        # NOR-Konfiguration
   - cfg:u          # Systemkonfiguration (Sprache, Region)
   - csnd:SND       # CSND-Audio
   - dsp::DSP       # NDSP-Audio (Haupt-Backend)
   - frd:u          # Freundesliste
   - fs:USER        # Dateisystem (SDMC, RomFS)
   - gsp::Gpu       # PICA200-GPU (Grafik)
   - gsp::Lcd       # Steuerung des LCD-Bildschirms
   - hid:USER       # Eingaben (Tasten, Pad, Touch)
   - http:C         # HTTP-Client
   - ir:rst         # Infrarot (C-Stick der New 3DS)
   - ir:u           # Infrarot allgemein
   - ir:USER        # Infrarot Benutzer
   - mic:u          # Mikrofon
   - mcu::HWC       # Hardware-Mikrocontroller
   - ndm:u          # Network Daemon Manager
   - news:s         # Benachrichtigungen
   - nwm::EXT       # Network Manager erweitert
   - nwm::UDS       # Network Manager UDS (lokales WLAN)
   - ptm:sysm       # Power/Timer Manager System
   - ptm:u          # Power/Timer Manager Benutzer
   - pxi:dev        # PXI-Gerätezugriff
   - soc:U          # Sockets (TCP/UDP-Netzwerk)
   - ssl:C          # SSL/TLS
   - y2r:u          # Umwandlung YUV nach RGB

Für ein einfaches Homebrew-Spiel sind diese Dienste unverzichtbar: APT:U, fs:USER, gsp::Gpu, hid:USER. Nimm dsp::DSP dazu, wenn du Audio nutzt, und cfg:u, wenn du die Systemkonfiguration ausliest. Wie bei den SVCs ist das komplette Set aber die sicherere Wahl, ein ungenutzter Dienst kostet nichts.

SystemControlInfo: Stack, Speicherstand und Abhängigkeiten

yaml
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: 0KB, wenn du direkt auf die SD schreibst (so machen es die meisten Homebrews). Setze eine Größe, wenn du die System-API zum Speichern nutzt.
  • StackSize: 0x40000 (256 KB) ist ein solider Standardwert. Erhöhe ihn, wenn deine App tief rekursiv arbeitet oder große Arrays auf dem Stack anlegt.
  • Dependency: die Title-IDs der Systemmodule, von denen deine Anwendung abhängt. Achtung: Die Title-IDs dürfen KEIN L-Suffix haben. 0x0004013000002402L statt 0x0004013000002402 zu schreiben, kann stille Fehler auslösen. Übernimm die Liste oben unverändert.

IORegisterMapping und MemoryMapping

yaml
  IORegisterMapping:
   - 1ff00000-1ff7ffff
  MemoryMapping:
   - 1f000000-1f5fffff:r

Diese Abschnitte legen fest, welche Speicherbereiche und I/O-Register erreichbar sind. Übernimm die Werte oben ohne Änderung, sie decken den Bedarf jedes Standard-Homebrews ab.

Die RSF an dein Projekt anpassen

Das RSF-Template oben ist allgemein und wiederverwendbar. Für ein neues Projekt änderst du nur die -D-Flags im Aufruf von makerom:

Flag -DWert bei 2048Anzupassen
APP_TITLE2048Name deiner App
APP_PRODUCT_CODECTR-H-2048eindeutiger CTR-H-XXXX
APP_UNIQUE_ID0xF2048eindeutiger Hexwert in 0xF0000-0xFFFFF
APP_ROMFSromfsPfad zu deinem romfs-Ordner
APP_SYSTEM_MODE64MB96MB, wenn New-3DS-exklusiv
APP_VERSION_MAJOR1Hauptversionsnummer

Die übrigen Werte (APP_ENCRYPTED=false, APP_CATEGORY=Application, APP_USE_ON_SD=true und so weiter) sind für alle Homebrews gleich.

Den CIA-Build im Makefile automatisieren

bash
# Variablen oben in 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

# CIA-Ziel — ein einziges „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)"

Endungen .bnr und .icn: Die von bannertool erzeugten Dateien tragen die Endungen .bnr (Banner) und .icn (Icon). Manche Tutorials nutzen .bin, korrekt sind aber .bnr und .icn, so machen es auch die etablierten Buildtools.

13. Typische Fallstricke und Fehler bei der 3DS-Homebrew-Entwicklung

Ich bin während der Entwicklung von 2048-3DS gegen jede einzelne dieser Wände gelaufen und hätte diese Liste gern von Anfang an gehabt. Hier kommen die frustrierendsten Fehler und wie du sie vermeidest, das spart dir beim ersten eigenen Homebrew-Spiel für die Nintendo 3DS etliche Stunden Debugging.

VRAM-Artefakte: der Geisterbug auf dem 3DS-Bildschirm

Symptom: Zufällige farbige Pixel erscheinen auf dem Bildschirm, in jedem Frame anders.

Ursache: C2D_TargetClear() vor C2D_SceneBegin() vergessen. Der VRAM der 3DS wird nicht automatisch initialisiert, er enthält Reste des vorherigen Frames oder beliebige Daten.

c
// FALSCH — VRAM-Artefakte garantiert
C2D_SceneBegin(top);
render_game();

// RICHTIG — immer vor dem Zeichnen löschen
C2D_TargetClear(top, couleur_fond);
C2D_SceneBegin(top);
render_game();

linearAlloc statt malloc: die Speicherfalle beim 3DS-Audio

Symptom: Der Ton bleibt stumm oder es knackt und rauscht.

Ursache: Audiobuffer auf der 3DS müssen mit linearAlloc() belegt werden. Auf Speicher von malloc() kann der DSP-Prozessor nicht zugreifen. Freigegeben wird entsprechend mit linearFree(), nicht mit free().

c
// FALSCH — der DSP kann diesen Speicher nicht lesen
u8 *audio_buf = malloc(size);

// RICHTIG — linearer Speicher, für den DSP erreichbar
u8 *audio_buf = linearAlloc(size);
// ... Verwendung ...
linearFree(audio_buf);

DSP_FlushDataCache vergessen: kaputtes Audio auf der 3DS

Symptom: Der Ton ist verzerrt, asynchron oder komplett falsch.

Ursache: CPU-Cache und DSP-Speicher der 3DS sind nicht kohärent. Nach jedem Schreibzugriff auf einen Audiobuffer musst du den Cache explizit flushen:

c
fread(snd->data, 1, chunk_size, f);
DSP_FlushDataCache(snd->data, snd->size);  // PFLICHT!

Falsches Audioformat: komplette Stille

Symptom: Kein Ton, oder Absturz beim Laden.

Ursache: Die WAV-Datei ist komprimiert (ADPCM, gekapseltes MP3 und so weiter) statt rohes PCM. Die 3DS versteht nur unkomprimiertes PCM. Prüfen und konvertieren:

bash
# Codec der WAV-Datei prüfen
ffprobe music.wav 2>&1 | grep "Audio:"
# Muss ausgeben: pcm_s16le (16 Bit) oder pcm_u8 (8 Bit)

# Ein MP3 in 3DS-taugliches WAV PCM umwandeln
ffmpeg -i music.mp3 -acodec pcm_s16le -ar 22050 music.wav

C2D_Color32 ist in C99 nicht konstant

Symptom: Compilerfehler „initializer element is not constant“ mit citro2d unter C99.

Ursache: C2D_Color32() ist in C eine Inline-Funktion, kein konstanter Ausdruck. Damit lassen sich globale Variablen in C99 nicht initialisieren. Die Lösung:

c
// FALSCH in C99
static u32 my_color = C2D_Color32(0xFF, 0x00, 0x00, 0xFF);

// RICHTIG — Makro MAKE_COLOR statt 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);

Die .3dsx läuft, die .cia stürzt ab: RSF-Berechtigungen

Symptom: Das Homebrew läuft als .3dsx über den Homebrew Launcher einwandfrei, stürzt als .cia aber sofort ab (oft mit der Meldung „ErrDisp: An error has occurred“ oder „SD Card was removed“). In Citra funktioniert dieselbe .cia.

Ursache: Das Format .3dsx erbt die weiten Berechtigungen des Homebrew Launchers. Die .cia hat eigene Berechtigungen, festgelegt in der RSF-Datei. Der Emulator Citra ignoriert diese Einschränkungen, deshalb läuft die .cia in der Emulation, aber nicht auf der Hardware. Drei Hauptursachen:

  • Unvollständiger SystemCallAccess: Ist ein SVC, den libctru nutzt, nicht deklariert, verweigert der ARM11-Kernel den Aufruf und beendet den Prozess. Nimm alle SVCs von 1 bis 125 auf.
  • Fehlender ServiceAccessControl: Ist ein Dienst (etwa dsp::DSP oder gsp::Gpu) nicht deklariert, schlägt srvGetServiceHandle() fehl und die Initialisierung der Bibliothek stürzt ab. Nimm alle Dienste aus Abschnitt 12 auf.
  • Logo: None in der RSF: Auf manchen Firmwares führt ein fehlendes Logo im ExeFS zum Absturz beim Start. Nutze Logo: Nintendo zusammen mit dem Flag -exefslogo.

Lösung: Nimm das vollständige RSF-Template aus Abschnitt 12, mit dem kompletten Set an SVCs, Diensten und Berechtigungen. Das ist der zuverlässigste Weg zu einer .cia, die auf echter Hardware läuft.

ndspWaveBuf nicht zurückgesetzt: der Sound spielt nur einmal

Symptom: Ein Soundeffekt läuft beim ersten Mal sauber, danach nie wieder.

Ursache: Nach der Wiedergabe bleibt der status des ndspWaveBuf auf „done“. Vor dem erneuten Abspielen musst du den Status zurücksetzen und den Cache erneut flushen:

c
snd->wave_buf.status = NDSP_WBUF_FREE;    // zurücksetzen
DSP_FlushDataCache(snd->data, snd->size);  // erneut flushen
ndspChnWaveBufAdd(channel, &snd->wave_buf); // erneut abspielen

romfsInit() vergessen: kein Asset lädt

Symptom: Alle fopen("romfs:/...") liefern NULL. Schriften, Texturen und Sounds laden nicht.

Ursache: romfsInit() wurde am Programmanfang nicht aufgerufen. Dieser Aufruf ist Pflicht, bevor du auf das RomFS zugreifst.

Falsche Maße der CIA-Banner-Assets

Für das CIA-Format sind die Maße strikt:

  • Icon: PNG mit exakt 48×48 Pixeln
  • Banner: PNG mit exakt 256×128 Pixeln
  • Banner-Audio: WAV PCM, möglichst kurz (etwa 3 Sekunden)

Stimmen die Maße nicht, scheitert bannertool stillschweigend oder erzeugt ein kaputtes Binary, an dem die Installation hängen bleibt.

14. Fazit: dein eigenes Homebrew-Spiel für die Nintendo 3DS

Ein Homebrew-Spiel für die Nintendo 3DS zu entwickeln berührt viele spannende Ecken der Programmierung: hardwarenahe Systemprogrammierung, 2D-Grafik mit citro2d, Low-Level-Audio über NDSP, eigenwillige Speicherverwaltung und ARM-Cross-Compilation. Die Grenzen der Plattform (wenig Speicher, zwei Bildschirme, proprietäre Formate) zwingen zu sauberem, effizientem Code, und ehrlich gesagt hat mir genau das riesigen Spaß gemacht.

Die 6 goldenen Regeln der 3DS-Homebrew-Entwicklung

  1. Trenne Spiellogik und Rendering. Die portable Logik in eine eigene Datei, das 3DS-Rendering in eine andere. Diese Strategie hat die Entwicklung von 2048-3DS überhaupt erst effizient gemacht.
  2. C2D_TargetClear vor jeder Szene. Immer. Ohne Ausnahme. Sonst: VRAM-Artefakte.
  3. linearAlloc für Audio, niemals malloc. Und vergiss nie DSP_FlushDataCache().
  4. Nur WAV PCM für sämtliche Audiodateien. Konvertiere MP3 und OGG, bevor du sie einbindest.
  5. Teste die .cia zusätzlich zur .3dsx. Die Berechtigungen unterscheiden sich, und ein in der RSF vergessener Dienst führt zum Absturz.
  6. Deklariere alle Systemdienste in der RSF-Datei, insbesondere dsp::DSP, csnd:SND, hid:USER und fs:USER.

Das Projekt 2048 für Nintendo 3DS setzt jedes Konzept aus diesem Tutorial praktisch um: Rendering auf zwei Bildschirmen, flüssige Animationen, mehrspurige Musik, binärer Speicherstand, Lokalisierung in 13 Sprachen, Achievements sowie Packaging als .3dsx und .cia. Ich habe es als Open Source veröffentlicht, damit es als Grundlage für deine eigenen Projekte dient.

2048 für Nintendo 3DS herunterladen, kostenlos als .3dsx und .cia. Wirf einen Blick in den Quellcode, dort siehst du all diese Konzepte in Aktion. Und wenn du selbst ein 3DS-Homebrew-Projekt startest: zeig es ruhig her, die Community ist offen und hilft gern weiter.

Zum Vergleich ein Ansatz ganz ohne eigenen Code: Unser Artikel Spiel mit ChatGPT erstellen begleitet Codex, die Programmier-KI von OpenAI, beim Bau eines kleinen Retro-RPGs im Stil von EarthBound, das im Browser spielbar ist.

Ressourcen für die 3DS-Homebrew-Entwicklung

  • devkitPro: https://devkitpro.org/, offizielle Toolchain für die 3DS-Homebrew-Entwicklung
  • libctru-Dokumentation: https://libctru.devkitpro.org/, vollständige Referenz der libctru-API
  • citro2d-Header: Die Header-Dateien in $DEVKITARM/../libctru/include/citro2d/ sind die beste Dokumentation der Grafikbibliothek
  • 3dbrew Wiki: https://www.3dbrew.org/, erschöpfendes Technik-Wiki zur Nintendo 3DS (Hardware, Formate, Systemdienste)
  • Offizielle Beispiele: https://github.com/devkitPro/3ds-examples, Beispiele zu Grafik, Audio und Netzwerk
  • bannertool: https://github.com/Steveice10/bannertool, Generator für Banner und Icons im CIA-Format
  • makerom: https://github.com/3DSGuy/Project_CTR, Assembler für CIA- und CCI-Dateien der Nintendo 3DS

Technische Daten der Nintendo 3DS

  • CPU: ARM11 MPCore (ARMv6K) @ 268 MHz, 2 Kerne (4 bei der New 3DS)
  • GPU: DMP PICA200 @ 268 MHz
  • RAM: 128 MB (256 MB bei der New 3DS), davon 6 MB dedizierter VRAM
  • Oberer Bildschirm: 400×240 Pixel (800×240 im stereoskopischen 3D-Modus)
  • Unterer Bildschirm: 320×240 Pixel, resistiver Touchscreen
  • Audio: 24 DSP-Kanäle, Stereoausgabe
Damien Flandrin Webentwickler seit 2010, Gründer von Gekkode und Email Impact. Jeder Artikel wird vor der Veröffentlichung an einem echten Projekt getestet. Kontakt
Newsletter

Neue Tests, Tutorials und Projekte, per E-Mail.

Reproduzierbare Tests, versionierter Code, datierte Ergebnisse. Niemals Spam.