Open-source GPU support for the Imagination BXM-4-64 GPU (PowerVR Rogue family, BVNC 36.56.104.183) built into the Allwinner A733 SoC. Written on Gentoo aarch64; the instructions below cover Debian, Fedora, Arch, Void and Gentoo alike.
The name comes from Latin aperio ("to open") plus celer ("fast"): an open driver with the clock raised.
The stack is tested live only on the Orange Pi 4 Pro. The A733 SoC and the
BXM-4-64 GPU are used in many other boards (Orange Pi Zero 3W, Radxa Cubie
A7 series, and others); nothing in this repository is specific to the
Orange Pi 4 Pro — the driver binds by the img,gpu device tree compatible,
and everything else is stock — so it is expected to work on other A733
boards, but this is not proven yet.
Testers are welcome: if you have another A733 board, please try it and report your results.
What this provides:
- An open-source kernel DRM driver ("apercel") built from the Mesa PowerVR
kernel driver. No vendor
pvrsrvkmbinary module; the vendor graphics stack is not required on the system. - A Mesa userspace stack covering Vulkan (the
imaginationVulkan driver) with zink for OpenGL, maintained in this repository as a patch against vanilla mesa-26.1.8. - Frequency/thermal handling for the GPU: the driver brings the GPU to its maximum documented clock (1.008 GHz from the device tree OPP table) on every power-up and throttles to 600 MHz at high temperature.
- Installation and verification tooling.
Current results on the Orange Pi 4 Pro: the GPU settles at 1008 MHz (previously the bootloader left it at 840 MHz and nothing ever raised it), the desktop benchmark reaches ~96 fps, and a stable 45–50 fps is achievable in a live Mindustry game.
Programs tested on the Orange Pi 4 Pro (the stack itself is distro-agnostic; the reference install ran on Gentoo aarch64, sway compositor):
| Program | Status | Notes |
|---|---|---|
| sway (compositor) | works | compositing on the GPU via zink |
| alacritty | works | terminal, smooth |
| foot | works | terminal, smooth |
| waybar | works | status bar |
| Helium browser (Chromium-based) | works | GPU accelerated |
| Mindustry | works | 45–50 fps in a live game |
| Factorio Space Age | runs | ~55 fps windowed (half-res data textures, see below) |
| Vesktop (Electron, Discord) | does not start | no workaround found yet |
Factorio performance notes (all measured with --benchmark-graphics):
streamed-atlas-physical-vram-size=32is required (see troubleshooting: black squares otherwise);texture-streamingmust stay enabled — withfalsethe whole atlas is resident (~1.3 GB) and the process OOMs on a 6 GiB board;- the frame is capped by a fixed ~40 ms present/sync cost of the GPU submission path (measured identical at 128x128 and 1024x600 window), texture resolution and window size change it little;
- half-resolution game textures: all
data/**.pnggame assets were downscaled to 50% in each dimension with nearest-neighbor (Pillow; ImageMagick is not installed; thecore/graphics/color_luts/,space-age/graphics/lut/16x16 3D LUTs and one unreadablecore/graphics/arrows/tip-arrow.pngare kept at original size — the LUTs must stay 16x16). Data files shrank 4.3 GB -> 2.5 GB and Factorio went from ~22 fps windowed to ~55 fps (~3.5x). Prototype declares still match (no load errors except two harmlesssprite rectanglewarnings). To revert:cp -a ~/factorio-data-original-backup/. ~/factorio/data/. mesa_gl_thread=true(drirc option, enabled by the install via~/.config/drirc/$XDG_CONFIG_HOME/drircor a per-game env var) moves GL submission off the main thread: +5 fps (16 → ~22); it also speeds up other CPU-bound GL applications;- windowed mode (~22 fps) renders faster than fullscreen (~16 fps) on this GPU (fullscreen fence path is slower).
Everything else that is not listed has not been tried.
driver/ kernel driver, out-of-tree kbuild, module "apercel"
src/ sources (upstream files keep their SPDX headers)
src/include/ support headers (pwrseq consumer, drm_gpuvm, drm_exec, uapi)
Makefile out-of-tree build; licensed GPLv2 (see LICENSE)
README-license.txt license statement
mesa/ userspace component
LICENSE MIT license (full text)
manifest.txt per-file notes on the changes
apercel-mesa.patch unified diff: vanilla mesa-26.1.8 -> apercel tree
apercel-mesa.patch.sha256
README.txt
tools/
verify-after-reboot.sh full post-boot check (module, binding, clock,
governor, thermal zone, EGL renderer)
gpu-status.sh short live status (clock, temperature, renderer)
conf/99-apercel.conf modprobe.d configuration to install
install.sh install everything and ask to reboot
uninstall.sh restore the previous state from backup/
backup/ is created locally by install.sh and is not part of the
repository.
- Kernel driver (driver/): GPLv2 — full text in the root LICENSE. Upstream per-file headers are "GPL-2.0 OR MIT" (Imagination Technologies Ltd., Linaro Ltd.); the upstream copyright headers are retained untouched.
- Mesa changes (mesa/): MIT — full text in mesa/LICENSE.
- Install scripts and tools (install.sh, uninstall.sh, tools/): GPL-2.0.
The instructions below are distro-agnostic and were tested on the
reference system: Gentoo aarch64 on the Orange Pi 4 Pro, kernel
6.6.98-sun60iw2 (the "orange-pi-6.6-sun60iw2" BSP tree with an
out-of-tree build directory kernelbuild/ inside the kernel source root),
OpenRC init, and a sway compositor. On Debian, Fedora, Arch, Void or any
other distribution everything works the same — the only distro-specific
parts are the package manager commands (below), the boot-time persistence
of the CPU governor snippet, and whether doas or sudo is your root
helper. Adjust paths to your system where needed.
Both components are built from source; you need a compiler toolchain, meson/ninja and a few development libraries. Everything installs as regular packages, nothing is distro-specific beyond the package names. Run only the block for your distribution:
Debian / Ubuntu (names may vary slightly between releases):
sudo apt install build-essential meson ninja-build python3 python3-mako \
flex bison pkg-config zlib1g-dev libdrm-dev libglslang-dev \
libclc-dev libvulkan-dev
Fedora:
sudo dnf install -y gcc make meson ninja-build python3 python3-mako \
flex bison pkgconf zlib-devel libdrm-devel glslang-devel \
libclc-devel vulkan-headers
Arch / Artix:
sudo pacman -S --needed base-devel meson ninja python-mako \
libdrm glslang libclc vulkan-headers zlib
Void:
sudo xbps-install -S base-devel meson ninja python3-Mako \
libdrm-devel glslang libclc zlib-devel
Gentoo:
sudo emerge --ask sys-devel/gcc sys-devel/make dev-util/meson \
dev-util/ninja dev-python/mako sys-devel/flex sys-devel/bison \
dev-util/pkgconf media-libs/libdrm dev-util/glslang \
llvm-runtimes/libclc media-libs/vulkan-headers
(For -Dmesa-clc=system a recent system glslang and libclc are
required; on older releases either upgrade them, or configure Mesa with
-Dmesa-clc=builtin instead, which builds the bundled compiler and needs
no extra system dependency.)
Prerequisites: a configured kernel tree for your A733 board with an
out-of-tree build directory (called kernelbuild/ below, produced by
make O=kernelbuild from the kernel source). Note: a stock distro kernel
does not contain the A733 SoC support — you need the Allwinner BSP kernel
for your board anyway. The examples below use doas; substitute sudo
on distributions where that is the default root helper. The three
helper modules are built from the same tree:
cd kernelbuild
make M=../drivers/gpu/drm/scheduler modules # gpu-sched.ko
make M=../drivers/gpu/drm modules # drm_shmem_helper.ko, drm_exec.ko
doas cp gpu-sched.ko drm-shmem-helper.ko drm-exec.ko \
/lib/modules/$(uname -r)/extra/ 2>/dev/null
doas depmod -a
(If your distro ships these helpers as gpu_sched, drm_shmem_helper and
drm_exec package modules, you can skip the build and use those instead;
they must expose drm_sched_*, drm_gem_shmem_* and drm_exec_*
exported symbols.)
cd /path/to/apercel # this repository
make -C /path/to/kernelbuild M=$(pwd)/driver modules -j6
doas cp driver/apercel.ko /lib/modules/$(uname -r)/extra/
doas depmod -a
Or, end-to-end (after building the module):
./install.sh # interactive, asks to reboot at the end
./install.sh --reboot # install and reboot without asking
The install scripts detect doas/sudo automatically: doas is used
when present, otherwise sudo.
The module loads /lib/firmware/apercel/rogue_<BVNC>_v1.fw. For a BXM-4-64
with BVNC 36.56.104.183 the expected file is:
/lib/firmware/apercel/rogue_36.56.104.183_v1.fw
The firmware binary is not redistributed in this repository (proprietary distribution terms). Current sources:
- Bespoke images for 36.56.104.183 ship with the Allwinner BSP: copy the file from the vendor firmware directory.
- Upstream
linux-firmwarealready carries a BXM-4-64 image but for a different BVNC (36.52.104.182); it is useful for cross-checks but is not a drop-in replacement for 36.56.104.183. When a matching binary lands upstream, copy it into/lib/firmware/apercel/under the name above and everything just works.
From a clean vanilla mesa-26.1.8 source tree:
cd mesa-26.1.8
patch -p1 -u < /path/to/apercel/mesa/apercel-mesa.patch
meson setup b-2618 --prefix=/usr/local \
-Dbuildtype=release \
-Dvulkan-drivers=imagination \
-Dgallium-drivers=zink \
-Degl=enabled -Dgbm=enabled -Dopengl=true \
-Dgles1=disabled -Dgles2=enabled \
-Dplatforms=x11,wayland -Dglx=dri -Dxlib-lease=disabled \
-Dllvm=disabled \
-Dmesa-clc=system -Dshader-cache=enabled -Dbuild-tests=false
ninja -C b-2618
doas ninja -C b-2618 install
doas ldconfig
The build is warning-free (0 warnings for the imagination directory).
Installed into /usr/local/lib64 (EGL, GLX, GBM, dri, gbm,
libvulkan_powervr_mesa) plus
/usr/local/share/vulkan/icd.d/powervr_mesa_icd.aarch64.json.
After a reboot:
./tools/verify-after-reboot.sh
./tools/gpu-status.sh
The bench used in both (EGL 1.5 / zink over the opensource Vulkan driver, desktop-like workload) reached 83.3 fps on the previous 840 MHz clock and 96 fps at 1.008 GHz; Mindustry reports 45–50 fps in a live game.
./uninstall.sh
It restores the previous kernel module (backup/*.ko), the previous modprobe.d configuration, and regenerates the module dependency database.
- On every power-up the PLL (
clk_parent, pll-gpu) is set to 1.008 GHz, the maximum rate listed in thegpu-opp-tablein the device tree. Previously the bootloader's 840 MHz was never reprogrammed. - A delayed work runs every 500 ms and reads
gpu_thermal_zone. - At 85 °C or hotter the clock is clamped to 600 MHz; at 75 °C or cooler it is raised back to 1.008 GHz (hysteresis stops boundary oscillation).
- GPU can never be slower than 600 MHz while throttled.
Screen-update stalls were traced to the schedutil CPU governor, which
hopped between ~400 MHz and the max rates while the compositor ran. Both
CPU clusters are switched to the performance governor at boot. The same
one-liner applies everywhere; only the persistence differs per init
system:
The snippet:
for policy in /sys/devices/system/cpu/cpufreq/policy*; do
echo performance > "$policy/scaling_governor" 2>/dev/null
done
systemd (Debian, Fedora, Arch, most others) — save as
/etc/systemd/system/gpu-cpu-perf.service (root), then
systemctl daemon-reload && systemctl enable --now gpu-cpu-perf:
[Unit]
Description=Set the CPU performance governor for the compositor
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'for p in /sys/devices/system/cpu/cpufreq/policy*; do echo performance > "$p/scaling_governor"; done'
[Install]
WantedBy=multi-user.target
OpenRC (Gentoo, Artix) — write the loop into
/etc/local.d/gpu-cpu-perf.start (remember
chmod +x /etc/local.d/gpu-cpu-perf.start) and ensure the local service
is enabled (rc-update add local default).
runit (Void) — put the loop into /etc/rc.local; Void runs it from its
runit rc-local job at boot.
The hook (written by install.sh) does two things at boot:
- pins both CPU clusters to the
performancegovernor (compositor latency after frequency hops measured ~1 s); - routes the GPU IRQ line (
IRQGPUin/proc/interrupts) to the last (biggest) cores of the big.LITTLE layout — submit latency improves measurably (grep IRQGPU /proc/interruptsto verify afterwards).
To revert at runtime: echo schedutil > .../scaling_governor for each
policy, and disable/remove the persistence piece above if you want
battery-oriented behavior instead.
Boards with 6-8 GiB RAM hit the OOM killer quickly when a heavy native
title (Factorio Space Age ~6 GiB of anonymous memory) runs next to a
compositor. The helper /usr/local/sbin/apercel-memory-tune (written by
install.sh) sets this up at boot:
- zram swap, 8 GiB, zstd compression, priority 5 — anonymous pages go to compressed RAM first (compression is ~5x here for the game's working set, so 6 GiB of application data costs ~1.5-3 GiB of resident memory);
- an existing file/network swap is left untouched with a lower priority — it only fills if zram is exhausted;
vm.swappiness=100,vm.page-cluster=0,vm.watermark_scale_factor=125so reclaim prefers swap-out instead of stalling.
Measured under load (~7 GiB anonymous churn): zram filled steadily, the system stayed interactive, no OOM.
To clean up: run each value back (swapoff /dev/zram0 && zramctl -r /dev/zram0, remove the helper and persistence hook from
uninstall.sh).
Common problems and fixes, in the order they typically appear:
The module does not load (modprobe: ERROR: could not insert apercel)
dmesgright after the attempt tells the exact reason. The common ones:Unknown symbol drm_sched_*/drm_gem_shmem_*/drm_exec_*— the three helper modules are not loaded. Check/lib/modules/$(uname -r)/extra/and/etc/modprobe.d/99-apercel.conf(it force-loads them ahead ofapercel).apercel: exports duplicate symbol ...— a leftover vendorpvrsrvkmmodule is loaded or installed.rmmod pvrsrvkm(must be unused), or uninstall the vendor module files, and reboot.- Nothing binds at all — check
/sys/bus/platform/drivers/apercel/after boot: the device node1800000.gpumust be listed there. If not, the device tree does not carry animg,gpucompatible node (mostly seen on A733 vendors that only enable the vendor path). - System boots slowly and then fails — look for
firmware: failed to load apercel/rogue_*.fwin dmesg: the firmware file is missing or misnamed (see the firmware section above).
The module loads but EGL/Vulkan reports nothing
- zink surface empty with
vkEnumeratePhysicalDevicesfailing: the Mesa build in use does not carryPVR_DRM_DRIVER_NAME "apercel". Rebuild the Mesa stack from this repository's patch (see the install section), then restart the compositor. - Both
llvmpipe (software)and the PowerVR renderer should be visible:eglinfoshows zink (performance) and llvmpipe (fallback). If only llvmpipe appears, the ICD json/usr/local/share/vulkan/icd.d/powervr_mesa_icd.aarch64.jsonis missing or the build was done with-Dvulkan-drivers=imaginationabsent.
Screen updates freeze for roughly a second
- CPU-side
schedutilgovernor clock drop. Setperformance(see the CPU governor fix above) — this is compositor latency caused by CPU frequency hops, not the GPU.
Factorio with texture streaming shows black squares / silhouettes
- The GPU's GL limit here is
GL_MAX_TEXTURE_SIZE = 4096(the Vulkan device reportsmaxImageDimension2D = 4096; zink mirrors it), while Factorio's streamed atlas defaults to a physical page of8192x4096. Every streamed-page allocation then fails with[OpenGL]: Error INVALID_VALUE, each frame afterwards logsINVALID_OPERATION, and un-streamed sprites render as black shapes. - Fix: cap the streamed page budget to the 32 MB variant, which Factorio
lays out as a single
4096x4096texture. In~/factorio/config/config.iniunder[graphics]setstreamed-atlas-physical-vram-size=32. - Alternative (more RAM, zero artifacts):
texture-streaming=false.
cma_alloc timeout messages on the console
- They come from the display subsystem (sunxi DE), not from the GPU stack; known and harmless in the tested configuration.
Vesktop does not start
- Not solved yet; Electron on this configuration refuses to run on the PowerVR/zink stack. Progress/debugging notes welcome.
A desktop GL application complains about OpenGL features/renderers
- Desktop OpenGL on this hardware is provided by zink (GL translated to
Vulkan on the fly).
glGetString(GL_RENDERER)will show PowerVR in the renderer through zink. If an application requires specific desktop GL versions/features, check its requirement list — zink covers desktop GL functionality-wise, subject to device capabilities, andMESA_GL_VERSION_OVERRIDEcan be used to present a higher version string when an app over-reports what it needs.
Anything unexpected
- Re-run
./tools/verify-after-reboot.shand./tools/gpu-status.shand inspectdmesg | grep -iE "apercel|bxm|pvr". For real bugs open a GitHub issue with those outputs.
This project builds on:
- the open-source PowerVR kernel driver maintained in Mesa (Imagination Technologies Ltd., Linaro Ltd.);
- Mesa and its zink pipeline;
- the Orange Pi 4 Pro BSP kernel (orange-pi-6.6-sun60iw2) for SoC integration;
- linux-firmware for the open firmware image build.
This project was inspired by the repository 8borane8/retrorangepi4pro — seeing that the community behind this hardware was alive is what made me start working on the driver.