Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3cea748
Add ports/iPod gitignore for build artifacts and Rockbox checkout
colemancda Jul 13, 2026
308954a
Add CRockbox shim header: the C surface Embedded Swift sees on Rockbox
colemancda Jul 13, 2026
e437e4c
Add CRockbox clang module map for the iPod port
colemancda Jul 13, 2026
25a4bb2
Add Rockbox shim: TLSF allocator, atomics, mem helpers, libm for armv…
colemancda Jul 13, 2026
a979281
Add iPod software rasterizer: RenderCommand list to RGB565 canvas wit…
colemancda Jul 13, 2026
d08d5cd
Add iPod Swift entry points: engine state, click-wheel cursor, scroll…
colemancda Jul 13, 2026
5476545
Add Rockbox C host: plugin loop, click-wheel input, intro/status/win-…
colemancda Jul 13, 2026
82f62c0
Add simulator stub standing in for the armv4t Swift engine
colemancda Jul 13, 2026
d5ccdf4
Add Rockbox plugin SOURCES listing (shim on device, stub on simulator)
colemancda Jul 13, 2026
38c3978
Add Rockbox plugin make fragment linking libjunkbot.a and TLSF
colemancda Jul 13, 2026
da4d2ac
Add sprite asset generator emitting sprites.bin and the offset table
colemancda Jul 13, 2026
caef0d9
Add script pinning the Rockbox tree at a fixed SHA
colemancda Jul 13, 2026
0d017ae
Add script syncing the plugin into the Rockbox tree and registering it
colemancda Jul 13, 2026
e422055
Add undefined-symbol audit against the shim allowlist
colemancda Jul 13, 2026
4c9c37e
Add script clearing host-native Rockbox tool binaries between sim and…
colemancda Jul 13, 2026
beb9df3
Add script launching the Rockbox UI simulator from the right directory
colemancda Jul 13, 2026
014cdc4
Add iPod port Makefile: armv4t Swift build, assets, Docker Rockbox st…
colemancda Jul 13, 2026
aebd7f7
Document the iPod Nano 2G Rockbox port
colemancda Jul 13, 2026
5bc7a48
Fix shim comment terminating early on an embedded end-of-comment token
colemancda Jul 13, 2026
e19ba29
Add LevelPack host tool package for serializing campaign levels
colemancda Jul 13, 2026
b2e8d0c
Add levels.bin codec shared between the host packer and the device de…
colemancda Jul 13, 2026
f13ba79
Add LevelPack main: write levels.bin and round-trip-verify through th…
colemancda Jul 13, 2026
f0fa592
Load levels from levels.bin instead of compiled-in level data
colemancda Jul 13, 2026
acc92e6
Load levels.bin alongside sprites.bin into the audio buffer
colemancda Jul 13, 2026
73bba31
Match the simulator stub to the new junkbot_init signature
colemancda Jul 13, 2026
2827aa5
Exclude generated level data from the plugin build and generate level…
colemancda Jul 13, 2026
082b37b
Document filesystem-loaded levels and the PLUGIN_RAM budget
colemancda Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ports/iPod/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
rockbox/
215 changes: 215 additions & 0 deletions ports/iPod/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#---------------------------------------------------------------------------------
#
# Junkbot -> Rockbox plugin for iPod Nano 2G (and other click-wheel iPods).
#
# Two-stage build:
# 1. `make swift` -- host swiftc compiles JunkbotCore + this port's Swift into
# one Embedded Swift object for armv4t-none-none-eabi (the Nano 2G's
# ARM940T), audits its undefined symbols, and archives libjunkbot.a. The
# tiny sprite offset table (build/SpriteAssets.swift) is generated first.
# 2. `make rock` -- syncs plugin/ + libjunkbot.a into a pinned Rockbox tree
# and runs Rockbox's own build (configure --target=ipodnano2g) to produce
# junkbot.rock. The arm-elf-eabi-gcc cross toolchain lives in a Docker
# volume (`make toolchain` builds it once, ~40min) because GCC 9.5 can't
# be built with an arm64 macOS host natively.
#
# make install IPOD=/Volumes/<name> -- copy the .rock + sprites.bin onto a
# mounted iPod.
# make sim -- Rockbox UI simulator build (C scaffolding + stub only).
#
# ASSETS: sprites.bin (~5.6MB RGB555 pixel data) is deliberately NOT linked into
# the .rock -- it is installed to /.rockbox/junkbot/sprites.bin and loaded into
# the audio buffer at runtime (see plugin/junkbot.c). Only the small offset
# table is compiled in.
#
#---------------------------------------------------------------------------------

ROCKBOX_SHA := 078a506dfd0deb18165a3ed80c7fcbdb3afb0d31
SWIFT_TOOLCHAIN ?= $(HOME)/Library/Developer/Toolchains/swift-6.3.2-RELEASE.xctoolchain
SWIFTC ?= $(SWIFT_TOOLCHAIN)/usr/bin/swiftc
NM ?= nm
# BSD ar mangles ELF members; the Swift toolchain's llvm-ar (next to swiftc)
# emits GNU format. (:= because make predefines AR=ar, which ?= defers to.)
AR := $(dir $(SWIFTC))llvm-ar
DOCKER ?= docker
DOCKER_IMAGE := debian:bookworm
DOCKER_VOLUME := rbdev
DOCKER_APT := build-essential texinfo flex bison wget curl xz-utils bzip2 \
patch file ca-certificates zip automake autoconf libtool \
libtool-bin perl python3

REPO_ROOT := ../..
BUILD := build

#---------------------------------------------------------------------------------
# Embedded Swift flags: ARMv4T soft-float, importing the tiny CRockbox shim
# module. Font.swift (SDL-only text layout) and LevelCatalog.swift (Foundation
# text-catalog scanning) are excluded -- matching ports/3DS -- since nothing
# here calls them and they don't compile freestanding.
#---------------------------------------------------------------------------------
SWIFTFLAGS := -target armv4t-none-none-eabi \
-enable-experimental-feature Embedded \
-wmo -Osize \
-module-name Junkbot \
-Xcc -ffreestanding \
-Xcc -fmodule-map-file=plugin/module.modulemap

CORE_EXCLUDE := Font.swift LevelCatalog.swift
# The generated level data (~1.6MB of entity-builder code every other port
# compiles in) alone overflows PLUGIN_RAM; this port ships it as levels.bin
# instead (see LEVELPACK below), so exclude it -- and EmbeddedLevelCatalog,
# whose whole body is already #if !hasFeature(Embedded), stays harmless.
GEN_EXCLUDE := LevelData.swift JunkbotLevelData.swift UndercoverLevelData.swift
CORE_SWIFT := $(filter-out $(addprefix $(REPO_ROOT)/Sources/JunkbotCore/,$(CORE_EXCLUDE)), \
$(wildcard $(REPO_ROOT)/Sources/JunkbotCore/*.swift)) \
$(filter-out $(addprefix $(REPO_ROOT)/Sources/JunkbotCore/Generated/,$(GEN_EXCLUDE)), \
$(wildcard $(REPO_ROOT)/Sources/JunkbotCore/Generated/*.swift))
# LevelBinCodec.swift is shared with the LevelPack host tool (single source of
# truth for the levels.bin field order).
LEVELPACK := tools/LevelPack
PORT_SWIFT := $(wildcard source/*.swift) \
$(LEVELPACK)/Sources/LevelPack/LevelBinCodec.swift
GEN_SWIFT := $(BUILD)/SpriteAssets.swift

.PHONY: all swift assets toolchain rockbox-tree sync rock sim run-sim install clean distclean

all: swift assets

$(BUILD):
@mkdir -p $@

#---------------------------------------------------------------------------------
# Assets: sprites.bin (installed to the device, not linked) + SpriteAssets.swift
# (the tiny per-sprite offset table, compiled into libjunkbot.a). A stamp file
# stands in for the group because macOS's default make 3.81 predates grouped
# (`&:`) targets.
#---------------------------------------------------------------------------------
$(BUILD)/assets.stamp: tools/gen_assets.py \
$(REPO_ROOT)/Sources/JunkbotCore/Generated/SpriteTable.swift | $(BUILD)
@echo generating sprite assets
python3 tools/gen_assets.py $(REPO_ROOT) $(BUILD)
@touch $@

$(BUILD)/SpriteAssets.swift $(BUILD)/sprites.bin: $(BUILD)/assets.stamp

# levels.bin: the campaign levels serialized by the LevelPack host tool (which
# runs JunkbotCore natively, imports the same embeddedLevels the other ports
# compile in, and round-trip-verifies the blob through the device decoder).
$(BUILD)/levels.bin: $(LEVELPACK)/Sources/LevelPack/main.swift \
$(LEVELPACK)/Sources/LevelPack/LevelBinCodec.swift \
$(REPO_ROOT)/Sources/JunkbotCore/Generated/LevelData.swift | $(BUILD)
@echo generating level assets
swift run -c release --package-path $(LEVELPACK) LevelPack $@

assets: $(BUILD)/sprites.bin $(BUILD)/levels.bin

#---------------------------------------------------------------------------------
# Stage 1: Swift -> armv4t object -> audited static archive.
# The archive is created with llvm-ar in GNU format so Rockbox's GNU ld accepts it.
#---------------------------------------------------------------------------------
$(BUILD)/junkbot.swift.o: $(CORE_SWIFT) $(PORT_SWIFT) $(GEN_SWIFT) \
plugin/module.modulemap plugin/rockbox_shim.h | $(BUILD)
@echo compiling Swift \(engine + port, armv4t\)
$(SWIFTC) $(SWIFTFLAGS) -c $(CORE_SWIFT) $(PORT_SWIFT) $(GEN_SWIFT) -o $@

$(BUILD)/libjunkbot.a: $(BUILD)/junkbot.swift.o scripts/check-undefined.sh
sh scripts/check-undefined.sh $< $(NM)
rm -f $@
$(AR) crs $@ $<

swift: $(BUILD)/libjunkbot.a

#---------------------------------------------------------------------------------
# Stage 2: Rockbox. GCC 9.5 (what Rockbox pins for arm-elf-eabi) can't be built
# natively on arm64 macOS, so there the toolchain and .rock build both run
# inside a Linux container (the toolchain persists in a Docker volume). On a
# native Linux host -- including CI -- neither needs Docker at all.
#---------------------------------------------------------------------------------
HOST_OS := $(shell uname -s)
RBDEV_PREFIX ?= $(HOME)/rbdev

rockbox-tree:
sh scripts/get-rockbox.sh $(ROCKBOX_SHA)

ifeq ($(HOST_OS),Linux)

toolchain: rockbox-tree
mkdir -p $(RBDEV_PREFIX)/dl
export MAKEFLAGS=-j$$(nproc); \
yes "" | rockbox/tools/rockboxdev.sh --target=a --prefix=$(RBDEV_PREFIX) \
--dlwhere=$(RBDEV_PREFIX)/dl --builddir=/tmp/rbbuild

sync: rockbox-tree swift
sh scripts/sync-plugin.sh $(abspath rockbox)

rock: swift sync
PATH="$(RBDEV_PREFIX)/bin:$$PATH" sh -c '\
mkdir -p rockbox/build-nano2g && cd rockbox/build-nano2g && \
{ [ -f Makefile ] || ../tools/configure --target=ipodnano2g --type=n; } && \
make -j$$(nproc) rocks && \
arm-elf-eabi-size apps/plugins/junkbot/junkbot.elf 2>/dev/null'
cp rockbox/build-nano2g/apps/plugins/junkbot/junkbot.rock $(BUILD)/
@echo "built $(BUILD)/junkbot.rock"

else

toolchain: rockbox-tree
$(DOCKER) volume create $(DOCKER_VOLUME)
$(DOCKER) run --rm -v $(DOCKER_VOLUME):/opt/rbdev \
-v $(abspath rockbox):/rockbox:ro $(DOCKER_IMAGE) bash -c '\
set -e; apt-get update -qq; \
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $(DOCKER_APT) >/dev/null; \
export MAKEFLAGS=-j$$(nproc); \
yes "" | /rockbox/tools/rockboxdev.sh --target=a --prefix=/opt/rbdev \
--dlwhere=/opt/rbdev/dl --builddir=/tmp/rbbuild'

sync: rockbox-tree swift
sh scripts/sync-plugin.sh $(abspath rockbox)

rock: swift sync
sh scripts/clean-host-tools.sh
$(DOCKER) run --rm -v $(DOCKER_VOLUME):/opt/rbdev \
-v $(abspath rockbox):/rockbox $(DOCKER_IMAGE) bash -c '\
set -e; apt-get update -qq; \
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
build-essential perl zip unzip wget ca-certificates python3 >/dev/null; \
export PATH=/opt/rbdev/bin:$$PATH; \
mkdir -p /rockbox/build-nano2g; cd /rockbox/build-nano2g; \
[ -f Makefile ] || ../tools/configure --target=ipodnano2g --type=n; \
make -j$$(nproc) rocks; \
arm-elf-eabi-size apps/plugins/junkbot/junkbot.elf 2>/dev/null || true'
cp rockbox/build-nano2g/apps/plugins/junkbot/junkbot.rock $(BUILD)/
@echo "built $(BUILD)/junkbot.rock"

endif

# UI simulator (host build; uses junkbot_stub.c). Rockbox's configure hardcodes
# gcc-16/gcc-ar-16/cpp-16 on Darwin whenever it detects Clang, assuming Homebrew
# GCC (`brew install gcc`) is on PATH.
sim: sync
sh scripts/clean-host-tools.sh
mkdir -p rockbox/build-sim
PATH="/opt/homebrew/bin:$$PATH" sh -c ' \
cd rockbox/build-sim && { [ -f Makefile ] || \
../tools/configure --target=ipodnano2g --type=s; } && $(MAKE)'
@# rockboxui reads plugins from simdisk/.rockbox/rocks (its simulated
@# device root), which the plugin build alone doesn't populate.
mkdir -p rockbox/build-sim/simdisk/.rockbox/rocks/games
cp rockbox/build-sim/apps/plugins/junkbot/junkbot.rock \
rockbox/build-sim/simdisk/.rockbox/rocks/games/

run-sim: sim
sh scripts/run-sim.sh

install: $(BUILD)/junkbot.rock $(BUILD)/sprites.bin $(BUILD)/levels.bin
@test -n "$(IPOD)" || { echo "usage: make install IPOD=/Volumes/<ipod>"; exit 1; }
cp $(BUILD)/junkbot.rock "$(IPOD)/.rockbox/rocks/games/"
mkdir -p "$(IPOD)/.rockbox/junkbot"
cp $(BUILD)/sprites.bin $(BUILD)/levels.bin "$(IPOD)/.rockbox/junkbot/"
@echo "installed junkbot.rock + sprites.bin + levels.bin to $(IPOD)"

clean:
rm -rf $(BUILD)

distclean: clean
rm -rf rockbox
126 changes: 126 additions & 0 deletions ports/iPod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Junkbot for iPod Nano 2G (Rockbox)

A [Rockbox](https://www.rockbox.org) plugin port of the shared `JunkbotCore`
engine, compiled as **Embedded Swift for `armv4t-none-none-eabi`** — the Nano
2G's FPU-less ARM940T — and linked into `junkbot.rock` by Rockbox's own build
system. The game renders through a click-wheel-scrolled viewport over the world
(levels are up to ~900×675 world px; the Nano's usable area is 176×120) on the
176×132 RGB565 LCD.

Architecturally this is the inverse of `ports/3DS` (where Swift owns `main`):
Rockbox calls the C `plugin_start()` in [plugin/junkbot.c](plugin/junkbot.c),
which owns the loop, click-wheel input, the scrolled world blit, the level
intro / win-lose prompts and the status strip, and calls the `@_cdecl` Swift
entry points in [source/Plugin.swift](source/Plugin.swift). Swift owns the game
state: the `GameEngine`, the current level, the aiming cursor, and the scrolled
viewport, and rasterizes the engine's `RenderCommand` list into the C-owned
canvas ([source/Renderer.swift](source/Renderer.swift)). Everything the armv4t
Swift output needs that a plugin doesn't have — allocator, non-atomic
`__atomic_*` stubs, `memcpy` family, `floor`/`ceil`/`round` — lives in
[plugin/rockbox_shim.c](plugin/rockbox_shim.c).

## Assets on the filesystem

Unlike every other Junkbot port, neither sprites nor levels are compiled into
the binary — both are loaded from `/.rockbox/junkbot/` into the ~28 MB audio
buffer at startup, and their pointers handed to `junkbot_init`:

- **`sprites.bin`** (~5.6 MB): every sprite frame and both background sheets as
RGB555 pixel data (one `UInt16` per pixel, bit 15 = opaque), generated by
[tools/gen_assets.py](tools/gen_assets.py). Only the tiny per-sprite offset
table (`build/SpriteAssets.swift`) is compiled in.
- **`levels.bin`** (~1.1 MB): all 121 campaign levels. Every other port
compiles the pre-parsed levels in as generated Swift, but that's ~1.6 MB of
code — over three times a plugin's entire 512 KB `PLUGIN_RAM` budget (the
first device link overflowed by 1.3 MB). [tools/LevelPack](tools/LevelPack)
(a host tool that imports `JunkbotCore` natively) serializes the same
`embeddedLevels` into a binary catalog instead, and round-trip-verifies the
blob through the device decoder at build time. The codec
([LevelBinCodec.swift](tools/LevelPack/Sources/LevelPack/LevelBinCodec.swift))
is one file compiled into both the tool and the plugin, so the field order
can't drift. Levels are decoded on demand, one at a time.

The Swift heap is a TLSF pool over the (separate) plugin buffer. The bitmap
font is Rockbox's own — the status strip and intro/win-lose text use
`rb->lcd_putsxy`, so no font asset is needed either.

## Controls

| Input | Action |
|---|---|
| scroll wheel | move cursor left / right (one stud) |
| LEFT / RIGHT | move cursor left / right (one stud) |
| MENU / PLAY | move cursor up / down (one brick) |
| SELECT | grab / drop the brick under the cursor |
| HOLD switch | pause menu (resume / restart / next / prev / quit) |

The cursor is the aiming reticle drawn over the world (there is no hardware
pointer); the viewport scrolls to keep it on screen. A grab that would land on
an invalid spot is refused by the engine, so the held brick simply keeps
following the cursor until a valid drop.

At the level intro screen: **SELECT** starts, **LEFT/RIGHT** change level.

## Prerequisites

- **Swift toolchain with an `armv4t-none-none-eabi` embedded stdlib** —
swift-6.3.2-RELEASE or newer (`swiftly install 6.3.2` or a toolchain from
swift.org). Point `SWIFT_TOOLCHAIN` at it if it isn't at the default path in
the Makefile.
- **Docker** — the `arm-elf-eabi-gcc` 9.5.0 cross-compiler is built by
Rockbox's `rockboxdev.sh` inside a Debian container into the `rbdev` Docker
volume (GCC 9.5 can't be built with an arm64 macOS host natively).
- **Homebrew GCC** (`brew install gcc`), only for `make sim` — Rockbox's
`configure` hardcodes `gcc-16`/`gcc-ar-16`/`cpp-16` on Darwin whenever it
detects Clang.

## Building

```sh
cd ports/iPod
make toolchain # once, ~40 min: arm-elf-eabi-gcc into the rbdev volume
make rock # libjunkbot.a (host swiftc) + Rockbox in-tree build
make install IPOD=/Volumes/<your-ipod> # → junkbot.rock + sprites.bin
```

Then on the device: **Plugins → Games → junkbot**.

Useful individual targets: `make swift` (just the audited `libjunkbot.a`),
`make assets` (regenerate `sprites.bin` + `SpriteAssets.swift`), `make sim`
(Rockbox UI simulator with the C test-pattern stub — needs SDL on the host),
`make run-sim`, `make rockbox-tree` (fetch the pinned Rockbox checkout).

## How the pieces fit

1. `make assets` runs [tools/gen_assets.py](tools/gen_assets.py), which reads
the canonical `Generated/SpriteTable.swift` and emits `build/sprites.bin`
(installed to the device) + `build/SpriteAssets.swift` (the offset table,
compiled in).
2. `make swift` compiles all of `Sources/JunkbotCore` (minus `Font.swift` /
`LevelCatalog.swift`) + `source/*.swift` + the offset table in one `-wmo
-Osize` invocation, audits the object's undefined symbols against the shim's
allowlist ([scripts/check-undefined.sh](scripts/check-undefined.sh)), and
archives `build/libjunkbot.a` with `llvm-ar`.
3. [scripts/get-rockbox.sh](scripts/get-rockbox.sh) pins a Rockbox SHA
(gitignored checkout under `rockbox/`);
[scripts/sync-plugin.sh](scripts/sync-plugin.sh) copies `plugin/` +
`libjunkbot.a` to `apps/plugins/junkbot/` and registers the plugin in
`SUBDIRS`/`CATEGORIES`.
4. Rockbox's `configure --target=ipodnano2g --type=n` + `make` builds
`junkbot.rock`; [plugin/junkbot.make](plugin/junkbot.make) links the prebuilt
`.a` plus the tree's TLSF allocator (the same mechanic `mikmod` uses).

## Audio status

**Not started** — `rb_audio_sfx` (the C hook `onPlaySound` calls) is a no-op.
The plan mirrors celeste-swift's iPod audio phase: an `sfx.bin` of concatenated
PCM clips installed alongside `sprites.bin`, played through Rockbox's channel
mixer (`mixer_channel_play_data` on `PCM_MIXER_CHAN_PLAYBACK`). The audio buffer
currently holds `sprites.bin`, so that phase needs a buffer split first.

## Targets other than the Nano 2G

Nothing here is Nano-specific except the default `configure` target: the SUBDIRS
guard enables any iPod click-wheel keypad (`IPOD_4G_PAD`/`IPOD_3G_PAD`/
`IPOD_1G2G_PAD`) with a 176×132+ 16-bit color LCD. Other click-wheel iPods
(4G color/Photo, Mini, Video, Nano 1G) should work with a different `--target`.
6 changes: 6 additions & 0 deletions ports/iPod/plugin/SOURCES
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
junkbot.c
#ifdef SIMULATOR
junkbot_stub.c
#else
rockbox_shim.c
#endif
Loading
Loading