Skip to content

Add support for the Anbernic stock OS (Allwinner H700) - #268

Open
Robilol wants to merge 2 commits into
rommapp:mainfrom
Robilol:anbernic-support
Open

Add support for the Anbernic stock OS (Allwinner H700)#268
Robilol wants to merge 2 commits into
rommapp:mainfrom
Robilol:anbernic-support

Conversation

@Robilol

@Robilol Robilol commented Aug 20, 2026

Copy link
Copy Markdown

What this does

Adds ANBERNIC as 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-260522 firmware image — rootfs and appfs inspected with debugfs, no mounting — and cross-checked against a real card and a real device. Nothing here is inferred from a wiki.

  • ROMs live in <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.
  • Two card slots. TF1 is mounted at /mnt/mmc, TF2 at /mnt/sdcard (/mnt/vendor/ctrl/mmc_new.sh). The launch script derives BASE_PATH from its own location, so a copy installed on TF2 manages TF2's ROMs with no configuration.
  • Saves. The stock RetroArch is patched by Anbernic: for content under a card mount point it rewrites the save path to <card>/saves_RA/<ROM path relative to the card>, and save states to states_RA/. The binaries contain the hardcoded sequence /mnt/sdcard | /mnt/mmc | %s/%s | %s/%s/%s | mkdir -p "%s" | saves_RA | states_RA followed by [Override] Cannot resolve save file path. A ROM in Roms/GBA therefore saves to saves_RA/Roms/GBA, so saves sort by platform folder and EmulatorFolderMap maps to Platforms, 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.
  • BIOS goes to RetroArch's system directory on the appfs partition, which /etc/init.d/launcher.sh mounts read-write at boot.
  • App listing. The stock launcher enumerates Roms/APPS filtering on sh|so|png|jpeg|jpg, so only .sh files appear and sibling directories stay invisible. The payload therefore sits in Roms/APPS/grout/, which is exactly how the stock apps are laid out (SimpleTerminal.sh + st/, tiny-scraper.sh + tiny_scraper/).
  • Input. The pad presents as a raw SDL joystick named ANBERNIC-keys. Button order was captured on device with INPUT_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.

libSDL2 2.28.5, libSDL2_ttf and libSDL2_image are already present in the stock rootfs; only libSDL2_gfx is missing and is shipped in lib/ as for every other ARM64 target. The binary needs nothing beyond GLIBC_2.17 and the rootfs is Debian bookworm, so the existing arm64 build target works unchanged.

Note on the alias map

Adding anbernic.Platforms to buildPlatformAliasMap creates exactly one new alias group repo-wide, supergrafxtg16: the stock firmware has no SGFX folder and runs SuperGrafx through PCE. GetPlatformAliases has 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.sh redirects stdout and stderr to logs/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 with task package:anbernic, and run end to end on a real RG35XX Plus running the stock firmware.

Verified on device:

  • app launches and renders — the SDL2 question this port hinged on
  • language selection, RomM authentication and token pairing
  • platform mapping screen, including folder creation under Roms/
  • input mapping, captured and confirmed with INPUT_CAPTURE
  • ROM download — the file lands in Roms/GBA/ and shows up in the stock launcher
  • save sync — 7 saves uploaded from saves_RA/Roms/GBA/ and saves_RA/Roms/GBC/, no errors

That 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 ./... and go test ./... all pass.

One note for anyone testing this: if the RomM library uses fs_slug values whose case differs from the saved directory_mappings keys, 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.

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.
@Robilol
Robilol marked this pull request as ready for review August 20, 2026 20:36
Robilol added a commit to Robilol/grout that referenced this pull request Aug 20, 2026
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.
Comment thread cfw/anbernic/anbernic.go Outdated
// 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"}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cfw/anbernic/anbernic.go Outdated
return basePath
}
for _, path := range cardPaths {
if info, err := os.Stat(filepath.Join(path, "Roms")); err == nil && info.IsDir() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Robilol Robilol Aug 25, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@BrandonKowalski BrandonKowalski self-assigned this Aug 22, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants