Skip to content

Building From Source

Samuel Costa edited this page Jul 27, 2026 · 1 revision

Building from source

Requirements

  • devkitARM + libctru (via devkitPro)
  • 3gxtool 1.3thepixellizeross-win/3gxtool (pacman package). Writes the 3GX$0002 container Luma3DS expects; the older 3GX$0001 tool is rejected by current Luma builds.

Build

⚠️ Spaces in the path break the build. The devkitPro toolchain fails (make[1]: /d/My Project: Is a directory) if any parent folder name contains a space. Keep the project on a space-free path.

Build through the devkitPro msys2 shell (not git-bash, not a plain Windows terminal):

/c/devkitPro/msys2/usr/bin/bash.exe -lc 'cd <project-no-spaces> && make'

The output is OcarinaCTRComposer.3gx (or whatever TARGET the Makefile sets).

Deploy

Copy the built .3gx to the SD card, one plugin file per Title-ID folder:

sdmc:/luma/plugins/0004000000033500/OcarinaCTRComposer.3gx

0004000000033500 is the USA Title ID for Ocarina of Time 3D. Enable the plugin loader in Rosalina (L + Down + SELECT → Plugin Loader → Enabled), launch the game, press SELECT.

Gotchas worth knowing before you touch the source

These aren't build-system quirks — they're 3DS-plugin-specific traps that cost real debugging time if you hit them blind:

  • __service_ptr — the 3dsx crt0 declares this; a raw .3gx doesn't get it automatically, so srvGetServiceHandle fails to link without a void *__service_ptr = NULL; somewhere in the plugin.
  • ThreadVars on the worker thread — a thread made with a raw svcCreateThread has no libctru ThreadVars block, so the first malloc/printf/hid/fs call svcBreaks in __syscall_getreent. Seed the thread-local storage manually at the very start of the thread function (magic 0x21545624 at TLS+0, reent pointer at TLS+8, fs_magic = 0 at TLS+0x10) before any newlib/hid/fs call. Give the thread ≥16KB of stack.
  • Never call hidInit(). On New 3DS it internally calls irrstInit(), and ir:rst conflicts with the running game — it freezes the game and locks the HOME button. Touch input uses a manual hid:USER shared-memory init instead (see the CTRComposer engine brief for the exact IPC calls).
  • Font mapping — when mapping the shared system font via APT IPC, the svcMapMemoryBlock target address argument must be 0; passing the address APT returned fails silently (no error, just garbage/blank text).
  • ZL/ZR are unreachable from a pure .3gx on New 3DS — they live behind ir:rst, which games without Circle Pad Pro support don't have in their exheader ACL, and a plugin can't patch the exheader. Use L/R combos for extra hotkeys instead.

For the full build brief (LCD registers, framebuffer format, the pause syscall, the whole reusable engine), see the game-agnostic CTRComposer repo.

Clone this wiki locally