Add support for the Anbernic stock OS (Allwinner H700) - #268
Conversation
Adds ANBERNIC as a CFW target so Grout runs on the original Anbernic firmware shipped on the Allwinner H700 handhelds, with no custom firmware installed. Conventions were read out of the V1.2.4 firmware image (rootfs and appfs inspected with debugfs) and cross-checked against a real card: - ROMs live in <card>/Roms/<UPPERCASE>, artwork in <rom dir>/Imgs. - The card is /mnt/mmc (TF1) or /mnt/sdcard (TF2); the launch script derives BASE_PATH from its own location so a copy on TF2 manages TF2. - The stock RetroArch is patched: for content under a card mount point it rewrites saves to <card>/saves_RA/<rom path relative to the card> and states to states_RA/, so saves sort by platform folder and EmulatorFolderMap maps to Platforms. - BIOS goes to RetroArch's system dir on appfs, which the launcher mounts read-write at boot. - The stock launcher lists only *.sh files in Roms/APPS, so the payload sits in a sibling grout/ folder and stays invisible, matching how the stock apps are laid out. The platform table is built from the firmware's own folder/core list. The pad presents as a raw SDL joystick with the same button order as the firmware's ANBERNIC-keys autoconfig (0=A, 1=B, 2=Y, 3=X, 4=L1, 5=R1, 6=Select, 7=Start, 8=Menu, 9=L2, 10=R2, d-pad on hat 0), confirmed on device. muOS runs the same hardware but enumerates those buttons starting at index 3, so its mapping is not reusable here.
This is a fork-only change; the upstream README is left untouched below the banner, and the anbernic-support branch that PR rommapp#268 proposes does not carry it.
| // cardPaths are the mount points the stock firmware uses for the two card slots: TF1 holds the | ||
| // OS and is always mounted at /mnt/mmc, TF2 is mounted at /mnt/sdcard when a second card is | ||
| // inserted (/mnt/vendor/ctrl/mmc_new.sh). | ||
| var cardPaths = []string{"/mnt/mmc", "/mnt/sdcard"} |
There was a problem hiding this comment.
I think this order is backwards. /mnt/sdcard should be the first element.
If BASE_PATH isn't set, flipping these makes the fallback prefer TF2 when a second card with Roms/ is present.
Since Grout.sh:24 exports BASE_PATH this should never really come in to play but better to have it than not.
| return basePath | ||
| } | ||
| for _, path := range cardPaths { | ||
| if info, err := os.Stat(filepath.Join(path, "Roms")); err == nil && info.IsDir() { |
There was a problem hiding this comment.
Continuation of my comment on line 28. When no BASE_PATH is set this loop checks each of the paths for the Roms directory. Since every h700 device has an SD Card in slot 1 this will always pick /mnt/mmc.
There was a problem hiding this comment.
I went a slightly different way than flipping the order, though. Reversing it makes the loop live but picks whichever card happens to hold ROMs, and if Grout is installed on TF1 while a TF2 with ROMs is present, a run without BASE_PATH would then manage TF2, the opposite of the "a copy installed on TF2 manages TF2" behaviour Grout.sh implements.
So the fallback now does in Go what the script does in shell: derive the card root from the executable's own path instead of $0. It's deterministic rather than a heuristic, correct for TF1 and TF2 alike, and the depth is the same one update.getInstallRoot already uses for this layout. It returns "" if the result has no Roms/, falling back to /mnt/mmc.
The previous fallback walked a fixed list of mount points looking for a Roms/ directory, but TF1 always holds the OS and always has one, so the loop could never reach /mnt/sdcard and was equivalent to returning /mnt/mmc outright. Rather than reverse the list, which would prefer whichever card happens to hold ROMs, derive the card root from the executable's own path the way Grout.sh derives it from $0. A copy installed on TF2 then manages TF2 in both cases. The depth matches update.getInstallRoot.
What this does
Adds
ANBERNICas a CFW target so Grout runs on the original Anbernic firmware — the stock OS that ships preinstalled on the Allwinner H700 handhelds — with no custom firmware involved.That firmware is the one most of these devices are actually running, since it is what comes in the box, so it seemed worth supporting alongside the CFWs.
How the conventions were established
Every path below was read out of the
RG35XX+P-EN-64G-V1.2.4-260522firmware image —rootfsandappfsinspected withdebugfs, no mounting — and cross-checked against a real card and a real device. Nothing here is inferred from a wiki.<card>/Roms/<UPPERCASE>, artwork in<rom dir>/Imgs. The platform table is built from the firmware's own/mnt/mod/ctrl/configs/CORES.txt, which lists each ROM folder with the core that runs it./mnt/mmc, TF2 at/mnt/sdcard(/mnt/vendor/ctrl/mmc_new.sh). The launch script derivesBASE_PATHfrom its own location, so a copy installed on TF2 manages TF2's ROMs with no configuration.<card>/saves_RA/<ROM path relative to the card>, and save states tostates_RA/. The binaries contain the hardcoded sequence/mnt/sdcard | /mnt/mmc | %s/%s | %s/%s/%s | mkdir -p "%s" | saves_RA | states_RAfollowed by[Override] Cannot resolve save file path.A ROM inRoms/GBAtherefore saves tosaves_RA/Roms/GBA, so saves sort by platform folder andEmulatorFolderMapmaps toPlatforms, as with ROCKNIX/ArkOS/Batocera. The same patch is present in the older January 2025 RetroArch build, so this is not new to the current firmware.appfspartition, which/etc/init.d/launcher.shmounts read-write at boot.Roms/APPSfiltering onsh|so|png|jpeg|jpg, so only.shfiles appear and sibling directories stay invisible. The payload therefore sits inRoms/APPS/grout/, which is exactly how the stock apps are laid out (SimpleTerminal.sh+st/,tiny-scraper.sh+tiny_scraper/).ANBERNIC-keys. Button order was captured on device withINPUT_CAPTURE:0=A, 1=B, 2=Y, 3=X, 4=L1, 5=R1, 6=Select, 7=Start, 8=Menu, 9=L2, 10=R2, d-pad on hat 0. muOS runs the same hardware but enumerates the same buttons starting at index 3, so its mapping could not be reused.libSDL22.28.5,libSDL2_ttfandlibSDL2_imageare already present in the stock rootfs; onlylibSDL2_gfxis missing and is shipped inlib/as for every other ARM64 target. The binary needs nothing beyondGLIBC_2.17and the rootfs is Debian bookworm, so the existingarm64build target works unchanged.Note on the alias map
Adding
anbernic.PlatformstobuildPlatformAliasMapcreates exactly one new alias group repo-wide,supergrafx↔tg16: the stock firmware has no SGFX folder and runs SuperGrafx throughPCE.GetPlatformAliaseshas no callers today, so there is no runtime effect, but flagging it since the map is shared across all CFWs.One behaviour change outside the new module
scripts/Anbernic/Grout.shredirects stdout and stderr tologs/stdout.log. This is not optional on this firmware: the stock launcher runs apps with> /dev/null 2>&1, so a panic or a loader error otherwise leaves no trace whatsoever. The stock apps do the same thing.Testing
Built with
task build:arm64, packaged withtask package:anbernic, and run end to end on a real RG35XX Plus running the stock firmware.Verified on device:
Roms/INPUT_CAPTURERoms/GBA/and shows up in the stock launchersaves_RA/Roms/GBA/andsaves_RA/Roms/GBC/, no errorsThat last one is the interesting confirmation: the
saves_RA/<ROM path relative to the card>scheme was derived by reading the patched RetroArch binary, and the sync found the real saves at exactly those paths. Save downloads (server to device) have not been exercised yet, only uploads.go build ./...,go vet ./...andgo test ./...all pass.One note for anyone testing this: if the RomM library uses
fs_slugvalues whose case differs from the saveddirectory_mappingskeys, Grout exits silently before drawing anything, which looks exactly like a crash. That is existing CFW-agnostic behaviour, reported separately as #267 and deliberately left alone here — it is not specific to this port, and fixing it means touching code shared by every CFW.Docs
docs/platforms/anbernic.md(mappings + quirks),docs/getting-started/install-anbernic.md, plus the usual index, nav and reference entries.