Technical reference
Kernel, Mesa, U-Boot, DTS, build pipeline
Build pipeline
make docker-RK3326 runs through the full LibreELEC-style stage system inside Docker. The current build installs 543 packages into the image.
toolchain cross-gcc / glibc / binutils, host stage
linux 6.12 LTS kernel + RK3326 DTS + ArchR patches
mali-bifrost out-of-tree mali_kbase + ArchR PM unbalance patch
mesa Panfrost (GLES 3.1, +speed +lto)
zlib (-ng) zlib-ng 2.2.4 in --zlib-compat mode (libz.so.1)
retroarch 1.x + libretro core suite
emulators Flycast, PPSSPP, melonDS, DraStic, Yabasanshiro, Mupen64Plus standalones
es EmulationStation (archr-linux/emulationstation-next)
image FAT32 BOOT (256 MB) + ext4 ROOT (4608 MB) + ext4 STORAGECurrent versions: gcc 14.2.0, Linux 6.12.94, Mesa 26.1.3, systemd 255.8, U-Boot v2025.10, pacman 7.0.0.
Output in target/<Ver of="imageBase" />-{original,clone}.img.gz.
Kernel
Source Linux 6.12 LTS (kernel.org, ArchR and Rockchip patches)
Toolchain gcc from the ArchR toolchain stage
Config projects/ArchR/devices/RK3326/linux/linux.aarch64.conf
HZ 300
PREEMPT CONFIG_PREEMPT=y (PREEMPT_VOLUNTARY A/B pending bench)
DEBUG all sanitizers / KASAN / KMEMLEAK / FTRACE off in release
DEBUG_FS y (intentional, needed for CMA debugfs and PSI introspection)
CMA 96 MB (raised from 64 MB after audit; see /docs/features)
ZRAM lzo-rle by default (zstd opt-in via memory-manager)
ZSWAP disabled (zswap.enabled=0 cmdline)DTS
Source: rk3326-gameconsole-r36s.dts (board) + rk3326-gameconsole-r3xs.dtsi (board family) + px30.dtsi (SoC) + 000-rk3326-dts.patch (ArchR overrides).
| Node | What we changed | Why |
|---|---|---|
cpu0_opp_table | Added 1512 MHz turbo OPP @ 1.4 V; kept full 408–1416 ladder | Reachable via "Enable CPU Overclock" |
vdd_arm regulator | regulator-max-microvolt 1.35 V → 1.45 V | Otherwise kernel rejects 1512 OPP silently |
gpu_opp_table | Restored 200/300/400 MHz lower OPPs; added 600/650 MHz top | Lower OPPs needed for boot; 650 MHz is daily turbo at 1.150 V |
dmc / dfi | Defined explicitly so devfreq can be controlled | Upstream px30.dtsi has them disabled |
gpu | Added clock-names="bus", resets, power_policy="coarse_demand", power_models | Required for modern panfrost/mali_kbase |
ethernet0 alias | Added | Future USB-Ethernet hot-plug naming |
| Power model | mali-simple-power-model + mali-g31-power-model | Thermal-aware DVFS |
Kernel patches
Notable ArchR-specific kernel patches (projects/ArchR/packages/linux-drivers/mali-bifrost/patches/6.12-LTS/ and projects/ArchR/packages/linux/patches/6.12-LTS/):
mali-bifrost/002-fix-unbalanced-regulator-clk-pm.patch: eliminatesunbalanced disables for vdd_logicwarnings whenvdd_logicis shared with rockchip-pm-domain. Adds agpu_power_heldflag tracking the driver's own enable state.linux/0005-drm-panfrost-Add-SYSTEM_TIMESTAMP*.patch: required by current Mesa forGL_TIMESTAMPqueries andVK_KHR_calibrated_timestamps.linux/0006-drm-panfrost-Add-cycle-counter-job-requirement.patch:VK_KHR_shader_clockplumbing.linux/0007-0010-drm-panfrost-Add-BO-labelling*.patch: debug aid; Mesa calls the new IOCTL even though the labels are debug-only.
Mesa
Built with +speed +lto in packages/graphics/mesa/package.mk. Configure flags:
-Dgallium-drivers=panfrost -Dvulkan-drivers=
-Dgles1=disabled -Dgles2=enabled
-Dglvnd=disabled -Dplatforms=wl
-Dglx=disabled -Dshader-cache=enabled
-Dopengl=true -Dgbm=enabled
-Degl=enabled -Dlibunwind=disabled
-Dbuild-tests=false -Ddraw-use-llvm=false-Dgallium-drivers=panfrost only, no software fallback shipped (LLVMpipe's overhead would dwarf hardware on G31). -Dvulkan-drivers= empty because PanVK isn't conformant on Bifrost v9.
Runtime env
Exported globally in /etc/profile.d/041-panfrost:
PAN_MESA_DEBUG=forcepack # G31-specific format-pack hint
MESA_NO_ERROR=1
MESA_SHADER_CACHE_DIR=/storage/.cache/mesa_shader_cache
MESA_SHADER_CACHE_MAX_SIZE=128MBPer-emulator MESA_GLTHREAD=true whitelist, only Flycast & PPSSPP standalone (see GPU driver and the in-repo docs/mesa-glthread-matrix.md).
libmali (alternative)
Out-of-tree mali_kbase r52p0-00eac0 with the regulator/clk PM patch. Switched at boot via /usr/bin/gpudriver --start reading /storage/.config/system/configs/system.cfg → gpu.driver. Auto-fallback if either driver fails to attach to the GPU node.
U-Boot
| Original | Clone | |
|---|---|---|
| Package | u-boot-legacy | u-boot |
| Source | Hardkernel BSP tree (archr-linux/hardkernel-uboot) | mainline U-Boot v2025.10, plus one patch for the K36 clone with eMMC |
| Defconfig | archr_rk3326_defconfig | rk3326-handheld_defconfig |
| Board selection | hwrev on SARADC channel 0 picks the DTB under /dtbs/ | same, through boot.scr |
Both variants ship boot.scr and extlinux/extlinux.conf, and both load the panel overlay from FDTOVERLAYS /overlays/mipi-panel.dtbo. The two loaders are not interchangeable: a clone will not boot the BSP build. ArchR ships both and the build picks by variant (make docker-RK3326 produces both images).
Image layout
MBR (msdos), 3 primary partitions, first partition starts at sector 32768
├── 1 FAT32 ARCHR 256 MB 16 KB clusters, kernel, DTBs, overlays/, U-Boot
├── 2 ext4 ARCHR_ROOT 4608 MB system, mounted read-only at runtime
└── 3 ext4 STORAGE rest writable: configs, ROMs, save states, screenshotsPartition sizes come from distributions/ArchR/options (BOOT_SIZE, ROOT_SIZE, SYSTEM_PART_START) and the labels from DISTRO_BOOTLABEL, DISTRO_ROOTLABEL and DISTRO_DISKLABEL. The third partition ships at 32 MB and grows to the rest of the card on first boot.
mkimage formats the boot partition with mkfs.vfat -F 32 -S 512 -s 32, which is 16 KB clusters, and then checks it with fsck.fat -n, read-only. Nothing in the build or on the device repairs that filesystem automatically.
Boot flow
0.0 POR / SPL (ROM-resident)
0.x U-Boot reads SARADC ch0 → board ID
0.5 loads board DTB + panel overlay from /flash/
0.7 initramfs splash (BMP via xxd, static aarch64 /init)
~7 kernel hand-off
~9 systemd target
~12 archr-autostart (quirks + governor + display + bluetooth + ssh check)
~17 EmulationStation
~19 ready to playTrace is written to /storage/.boot_last_step before each autostart script runs and erased on success, if the previous boot hung, the file at /storage/.boot_last_hang names the script that hung. Load it for bug reports.
Display panels
ArchR does not ship pre-built panel overlays. There are too many board and panel combinations to enumerate, so the overlay is built per device from the DTB of the console's own original card.
The image carries an empty overlays/ directory on the boot partition, with a README in six languages pointing at the generator (projects/ArchR/devices/RK3326/packages/u-boot/config/overlays/README.txt). extlinux.conf loads exactly one file from it:
FDTOVERLAYS /overlays/mipi-panel.dtboThe generator itself lives in two places, both running the same archr_dtbo.py:
- the browser version at /overlay-generator, which runs the script under a WebAssembly Python runtime
archr-dtbo-offline.zip, the same script as a terminal tool with thefdtlibrary and the base DTBs bundled
The device also carries a copy at /usr/libexec/generic-dsi/archr_dtbo.py, installed by the generic-dsi package.
The option string encodes the console and the board flags, for example SDORIG for an original R36S, SDCLONE for a clone and SDORIG-NAm for a Soysauce board.
Pitfalls
Edge cases that bit during development. None of them apply to a normal release flash, they only matter if you're hacking on the build.
Hunk header counts in DTS patches matter. A wrong @@ -X,Y +A,B @@ count causes patch to silently abort the rest of the hunks, recovered "garbage". Validate with patch --dry-run --verbose on a pristine source after every DTS edit.
turbo-mode OPPs hide behind cpufreq/boost. Setting scaling_max_freq=1512000 directly does nothing if boost=0, the freq isn't in scaling_available_frequencies until you write 1 to cpufreq/boost. ArchR's set_cpu_max_freq highest reads both scaling_available_frequencies and scaling_boost_frequencies.
vdd_arm regulator-max gates OPP availability silently. If an OPP requests a voltage above regulator-max-microvolt, kernel drops the OPP with no dmesg, and policy_has_boost_freq() returns false → cpufreq/boost sysfs file is never created.
The FAT warnings after a resize are expected. fs-resize runs fatlabel -i -r to give the boot partition a fresh volume ID, and fatlabel prints two complaints while doing it: the cluster count is below the FAT32 minimum, and the primary boot sector differs from the backup. Both are on purpose. The 256 MB boot partition uses the same out-of-spec FAT32 geometry as the rest of the RK3326 ecosystem, because the vendor U-Boot builds are only tested against that geometry, and the boot-sector difference is the kernel vfat dirty flag, which is written to the primary sector only while /flash is mounted read-write. fs-resize writes the same note into /flash/fs-resize.log.
Do not "fix" the boot filesystem with fsck.fat -a -w. Rewriting the geometry to something spec compliant is what breaks boot on the vendor loaders. Both mkimage and the device side check the filesystem read-only, with fsck.fat -n.
Mesa shader cache root must exist. Mesa won't mkdir MESA_SHADER_CACHE_DIR itself, just falls through to no-cache mode silently. runemu.sh creates /storage/.cache/mesa_shader_cache if missing.
panfrost_devfreq needs low GPU OPPs to initialize. Removing 200/300/400 MHz OPPs (to force a higher floor) blocks GPU init at boot, the driver expects the lowest published OPP to be reachable. Keep the ladder; force a high floor at runtime with set_gpu_min_freq highest instead.
PREEMPT vs PREEMPT_VOLUNTARY is unbenched. The kernel config sets CONFIG_PREEMPT=y. Whether PREEMPT_VOLUNTARY would do better on the A35 is an open question, pending an A/B run with the on-device bench.
Mali_kbase regulator unbalance is a kernel patch, not a quirk. Without 002-fix-unbalanced-regulator-clk-pm.patch, every PM transition emits unbalanced disables for vdd_logic and Enabling unprepared clk_gpu, audible as crackle in Mario 64.