Imagination Blog

Scaling the open-source PowerVR Vulkan driver to new GPU architectures

Written by Ashish Chauhan | Jul 17, 2026 12:52:06 PM

This article summarizes the foundational work we’ve been doing to support multiple GPU architectures (multi-arch) in the open-source PowerVR Vulkan driver in Mesa. While this work does not instantly unlock a new GPU architecture by itself, it does put the structure in place so we can add newer hardware cleanly in the future. We anticipate the first Volcanic cores to be supported next year.

What is multi-arch?

Multi-arch means one Vulkan driver codebase and one packaged driver that can support multiple PowerVR GPU architectures. As of today, the open-source driver supports only one of our architectures, Rogue, and, when there is only one supported architecture, it is easy for what is intended to be common code to end up including architecture specific details and picking up hidden dependencies. The changes that we’re making lay the groundwork so that future architectures (e.g. Volcanic) can be added without either rewriting the driver, adding top-level architecture-specific dispatching everywhere or growing big "if (rogue) ... else ..." ladders across the codebase. The changes are about keeping architecture-specific pieces well contained and making architecture-dependence explicit, which helps keep the driver maintainable.

The model we follow is the one described in the Mesa PowerVR multi-arch doc [1]. A small set of files are compiled multiple times, once per architecture. In PowerVR those are the pvr_arch_*.c files. They define functions prefixed with pvr_arch_, where the arch part is a placeholder expanded using defines in the corresponding headers, supported by macros in pvr_macros.h. The intent is that these functions are called from architecture-specific entry points routed through the common Vulkan dispatch-table code which can call both arch-specific and arch-agnostic code without mixing everything together.

The case that needs a bit more care is when architecture-agnostic code needs to call architecture-specific code. For that we use PVR_ARCH_DISPATCH and PVR_ARCH_DISPATCH_RET. These macros need to see the per-arch definitions for all architectures, and the pattern used to make that work is to define a PER_ARCH_FUNCS(arch) macro in each source file that needs to use the dispatch macros and instantiate it once per architecture.

PowerVR specific choices

Our approach is similar to the path taken by other Vulkan drivers in Mesa, with a few PowerVR-specific choices. For example, architectures are named symbolically rather than numerically because there is not a clean numerical split that maps across generations. We also make use of PVR_PER_ARCH() style macros as part of the plumbing. To reduce churn while landing the infrastructure, many functions were introduced as PVR_PER_ARCH(foo) and temporarily aliased with #define pvr_foo PVR_PER_ARCH(foo), so call sites did not have to change immediately. The longer-term direction is to move toward “code block style” defines, so it is visible what is per-arch without making function calls noisy.

Setting up the basic structure

Before the driver could reasonably become multi-arch, we split out the parts that cannot be architecture specific. Most of pvr_device is architecture specific, but physical device and instance cannot be architecture specific, so pvr_instance and pvr_physical_device were broken out of pvr_device.c into their own modules. We also moved some architecture-specific details from headers into source files so less of the driver needs to be built in a hardware-specific way, including the work that removed the need for a wide ranging pvr_private.h include.

 Another enabling change was storing the architecture in device-info. Right now, all supported devices are Rogue, but once we add more architectures we will need a runtime way to know which architecture we are dealing with. This is what will let us select per-arch tables and per-arch operations cleanly, and it will avoid pushing architecture knowledge into places that should not need it.

Setting hardware definitions

After the basic structure was in place, the next focus was reducing accidental Rogue-only dependencies in shared code. A lot of the prep work was about being disciplined with hardware definitions, so the hardware definition includes and helpers were moved to where they are needed (for example, toward the end of source files). This reduced the chance that shared code silently becomes tied to Rogue. We do not yet have a perfect way to handle hardware definitions that are common across architectures. Because of this, there are a few places where the code intentionally defines PVR_BUILD_ARCH_ROGUE and includes headers that require an architecture to be defined.

Alongside cleaning up the hardware definitions throughout the code, we restructured the hardware definitions file layout, so adding new GPUs in the future will not become unwieldy by having all the files in a single directory. This work removed rogue_hwdefs.h and instead included the separate files in pvr_csb.h, with the intention of making it easier to have separate directories per GPU architecture.

Handling architecture-specific data

Once more than one architecture was enabled, another practical issue showed up quickly: some  architecture-specific data was stored inside structures that were not architecture-specific. We addressed this for the border table and the clear state. In both cases, the issue was the same: the data was architecture specific, the size varied by architecture, and it couldn’t live directly inside a shared structure in a fixed way. The fix was to store pointers to the architecture-specific data instead, so the shared device object remains shared while the per-arch data can vary.

The format-table work was another multi-arch preparation step. pvr_formats.[ch] was reworked, so formats can be queried and reasoned without needing hardware details. PBE details were split out from the main table because they are not relevant for all GPUs. Binding support moved from a single supported bit to per-binding flags (vertex-buffer, sampler-view, render-target, depth-stencil, storage-image). Always invalid entries like compressed PBE formats were not stored. Helpers were added to query limits based on device-info.

Optimising winsys

With those pieces in place, we then simplified the winsys side, which abstracts the kernel driver being used, so multi-arch does not require dispatch logic throughout the driver. In the powervr winsys, uses of PVR_ARCH_DISPATCH were mainly there to reach kernel stream definitions for KMD_STREAM_HDR. Using the per-arch infrastructure here does not buy much, because the code runs during job context initialisation and the header itself is mostly a simple field plus padding while the real firmware interface content is already open-coded. To solve it, dispatching was dropped in that path, and the winsys stopped depending on the kernel stream definitions.

For the pvrsrvkm winsys, the situation was different because the kernel driver does not abstract away the same hardware details. Instead of trying to remove architecture knowledge entirely, we provide specialized per-arch pvr_winsys_ops structures that handle each architecture individually. This moved dispatch out of individual winsys functions and into selecting the correct ops structure per architecture, which reduces complexity and will make the winsys easier to maintain as more architectures are added.

Adding new definitions for Volcanic GPUs

On the new hardware side, an initial set of hardware definitions for Volcanic were added, with these more or less directly corresponding to what already exists for Rogue. This work is not complete enough to drive the GPU yet, but it is a step along the way. Some code paths were also made Rogue-only (like blitting and clearing) because for Volcanic the plan is to use vk_meta, so it did not make sense to spend time trying to clean up the older path ahead of time.

Overall, the direction across these changes is consistent: keep architecture-specific pieces behind a clear per-arch model (pvr_arch_*.c, PVR_PER_ARCH, and the dispatch or ops patterns), limit where hardware definitions are used to ensure shared code stays shared, and move architecture-specific decisions into device-info, per-arch tables, and per-arch operations. The Volcanic definitions start landing in a way that helps bring-up activities and the winsys changes reduce unnecessary dispatching while still handling the places where the kernel interface needs architecture-aware setup. 

Future work & acknowledgements

With the groundwork in place, the next step is to build upon this foundation work by filling out the remaining architecture-specific pieces and using the new framework to bring up support for Volcanic.  Special thanks to Erik Faye-Lund, from Collabora, for leading the overall design and driving the changes that established the multi-architecture structure in the PowerVR Vulkan driver. I would also like to thank the supporting team members for their contributions, discussions, and reviews throughout this work.

Supporting code changes:
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39526
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39348
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/39035
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/38922
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/38888
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/38832
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/38352
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/37675
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/37554
https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/37432