type0labs-dev/go-horizon

Experimental Go runtime port to Nintendo Switch / Horizon: libnx, CGO, reproducible patch and hardware test harness

★ 0Forks 0PythonGitHub ↗Compare
experimentalgolanghomebrewhorizonnintendo-switch

README

Go → Horizon

Experimental Go 1.24.5 runtime bring-up for Nintendo Switch / Horizon, using libnx and CGO. This is a runtime spike, not a complete Go platform port. Not affiliated with Nintendo, the Go project, devkitPro, or Eden.

A single integrated nine-stage suite has completed on a physical Switch: bootstrap, C file I/O, CGO callbacks, channels at GOMAXPROCS 1 and 3, timers, arena boundaries, stack/GC, and a 120-second stability workload. The earlier Eden matrix and its known failures are documented separately in validation/README.md. Do not interpret one hardware run as production readiness or full standard-library support.

What is here

  • A complete patch against a pinned upstream Go revision, including runtime, syscall stubs, CGO/TLS integration, and target registration.
  • Standalone C/libnx reference and Hello Go NRO builds with ASET/NACP metadata.
  • Identified, flushed test reports and a verbose on-device test console.
  • An isolated Eden runner with hashes, timeout handling, and explicit rejection of incomplete reports, crashes, or missing worker-shutdown confirmation.
  • A hardware report without console credentials, keys, firmware, or personal paths.

Build

Use Linux, Git, Bash, Python 3, a bootstrap Go installation, and a separately installed devkitPro toolchain with libnx, Switch tools (elf2nro, nacptool), and PIC newlib/libsysbase. No SDKs or compiler binaries are bundled. See the tested versions below. Use paths without whitespace: CGO linker flags are whitespace-delimited.

git clone https://github.com/type0labs-dev/go-horizon.git
cd go-horizon
export GOROOT_BOOTSTRAP=/path/to/bootstrap-go
bash prepare-go.sh

export DEVKITPRO=/opt/devkitpro
bash spike/build-c.sh
bash spike/build-hello.sh
python3 spike/check-elf.py

prepare-go.sh checks the patch checksum and exact upstream revision, refuses an existing go/, applies the patch, and builds the host toolchain. It does not modify your system Go. The port is pinned to Go 1.24.5; this repository does not provide ongoing upstream security maintenance for that old version.

Defaults use $DEVKITPRO/devkitA64 and $DEVKITPRO/tools/bin. Override as needed:

export DEVKITA64=/path/to/devkitA64
export HORIZON_TOOL_PREFIX=/path/to/aarch64-none-elf-gcc/bin/aarch64-none-elf-
export HORIZON_SWITCH_TOOLS=/path/to/switch-tools/bin

The original tests used Go 1.24.5, Arm GNU 14.2.Rel1 / GCC 14.2.1, libnx 4.12.0, and devkita64-newlib 4.6.0.20260123-4. These are observed versions, not a guarantee that every other SDK combination works. The builds set CGO_ENABLED=1, GOOS=horizon, GOARCH=arm64, GOARM64=v8.0, and GOEXPERIMENT=nospinbitmutex.

If your SDK does not contain lib/pic/libsysbase.a, obtain compatible dependencies rather than adding syscall wrappers that call themselves. hello.build.json records toolchain versions and library/ELF/NRO hashes.

Run on a Switch

Use your existing homebrew environment in application mode, not applet mode: the Go arena alone is 512 MiB. This project does not install or modify firmware. Back up important data and treat this as experimental software.

  1. Copy spike/hello/hello.nro to sdmc:/switch/hello/hello.nro.

  2. Create sdmc:/switch/hello/request.txt with these three lines, replacing the first with a new identifier for every run:

    my-unique-run-001
    all
    interactive
    
  3. Open Hello Go - Horizon spike. Watch the nine stages and the stability counters. Allow roughly 2–3 minutes without suspending the console.

  4. At the final screen, press + to finish. Then retrieve sdmc:/switch/hello/result.txt.

The test reads request.txt (supplied by you) and writes result.txt and a 64 KiB payload.bin in that test directory. It overwrites previous results, so archive them before starting again. The standalone reference.nro uses the same directory; set the requested case to c for the C reference.

Success requires a matching RUN identity, all nine CASE PASS lines, EXIT: Go workers joined; returning through libnx, a final SUMMARY: PASS, and an observed normal return to the menu. A PASS written before a crash does not approve a run. A runtime/kernel crash may bypass the final screen; save the report and photograph any error. The report cannot itself prove an OS exit status or that the menu resumed successfully.

all runs the stages once in one process. It does not reproduce the 83-process repeated Eden matrix. Individual cases are boot, cgo, channels1, channels3, timers, arena, memory, and stability. Omit the third request line for no console display/pause. The negative shutdownblocked case is deliberately excluded from all.

Test on Eden

Supply your own Eden CLI and a graphical X11/XWayland session. No keys or firmware are copied into the isolated test profiles.

export EDEN=/path/to/eden-cli
python3 spike/run-eden.py spike/hello/hello.nro --case all --timeout 240 --display-test
python3 spike/test-all.py                  # 80 short runs + 3 x 120-second runs
python3 spike/test-runner.py               # mocked acceptance/rejection checks
python3 spike/summarize.py spike/results/suite-YYYYMMDD-HHMMSS.json

--display-test exercises console rendering without waiting for controller input. Each real run archives artifacts under ignored spike/results/. VSync is explicitly overridden in the isolated profile. A C reference failure stops the matrix; later failures are collected and always reject the suite. An incomplete matrix never establishes viability. Do not publish raw cores or emulator profiles without reviewing their contents for private data.

Important limits

  • Fixed 512 MiB arena, 39-bit address-space assumptions, polling semaphores, no asynchronous goroutine preemption, and unsupported syscalls returning ENOSYS.
  • Timers are monotonic; civil time still derives from uptime. Local timezone is UTC; timezone-file loading is unsupported. File tests use C/libnx, not a complete os port. Network, game rendering/audio, and performance remain unvalidated.
  • Normal main remains on the original OS thread. On exit, registered workers stop cooperatively at Go wait points; the port joins them, frees resources, restores TPIDR_EL0, and returns through libnx. The registry is limited to 256 threads over a run; this is not general thread-retirement support.
  • Joins have a five-second deadline, but a negative busy-loop test ended in an outer Eden timeout without confirming that internal deadline. Never assume arbitrary noncooperative code can be shut down cleanly. Panic and off-main exits still terminate the process rather than returning to the loader.
  • External C-created callback threads, custom CGO traceback context, and arbitrary standard-library packages are not supported by this evidence.
  • Hardware coverage is one integrated run on one console; firmware/model details were not collected. Additional independent reports are welcome.

Contributing and licenses

Start with CONTRIBUTING.md. Repository-owned code is BSD-3-Clause under LICENSE. Go-derived changes retain the Go Authors' notices, Go license, and patent grant. See THIRD_PARTY.md. No copyrighted game content is included.

Contributors

type0labs-dev

Issues