(how-to-enable-graphics-for-snaps-on-a-device)=

# How to enable GPU support for snaps using gpu-2404

This document will guide you through the process of assembling a
`gpu-2404` snap. With this snap, your device will be ready
to run graphical snaps like `ubuntu-frame` or any other *Mir*-based
compositor in a snapped environment.

To this end, we will accomplish the following in order:

- How to assemble a `gpu-2404` provider snap
- How to test a `gpu-2404` provider snap
- How to test the confinement of your *Mir*-based snap that relies on the provider snap

## Prerequisites

Before we get started, let's make sure that we have everything that we require.

### Development board

For starters, make sure that you have some sort of development board on hand.

### GPU support check

Next, make sure that this board runs a `snapd`-enabled Ubuntu image with
a kernel that includes GPU support. To do this, we'll run a series of checks.

First, you should ensure that *at least* `/dev/dri/card0` exists, but you
may have more depending on your setup. You may check this from the command line.

Next, install the [graphics-test-tools](https://snapcraft.io/graphics-test-tools) snap:

```shell
sudo snap install graphics-test-tools --channel 24/stable
```

This snap will provide you with `graphics-test-tools.drm-info`. Run it and you
should see something like the following:

```text
Node: /dev/dri/card1
├───Driver: amdgpu (AMD GPU) version 3.54.0 (20150101)
│   ├─── ...
├───Device: PCI 1002:747e Advanced Micro Devices, Inc. [AMD/ATI] Device 747e
│   └───Available nodes: primary, render
├───Framebuffer size
│   ├───Width: [0, 16384]
│   └───Height: [0, 16384]
├───Connectors
|   |   ...
│   ├───Connector 3
│   ├───Object ID: 101
│   ├───Type: HDMI-A
│   ├───Status: connected
│   ├───Physical size: 530x300 mm
│   ├───Subpixel: unknown
│   ├───Encoders: {3}
│   ├───Modes
│   │   ├───1920x1080@60.00 preferred driver phsync pvsync
│   │   ├───1920x1080@60.00 driver phsync pvsync 16:9
│   │   ├───...
|   |   ...
├───Encoders
│   ├───Encoder 0
│   │   ├───Object ID: 82
│   │   ├───Type: TMDS
│   │   ├───CRTCS: {0, 1, 2, 3}
│   │   └───Clones: {0}
|   |   ...
├───CRTCs
│   ├───CRTC 0
│   │   ├───Object ID: 72
│   │   ├───Legacy info
│   │   │   ├───Mode: 1920x1080@60.00 preferred driver phsync pvsync
│   │   │   └───Gamma size: 256
|   |   ...
└───Planes
    ├───Plane 0
    │   ├───Object ID: 40
    │   ├───CRTCs: {3}
    │   ├───Legacy info
    │   │   ├───FB ID: 0
    |   ...
```

You want to make sure that you have:

- 1 Node
- 1 or more Connector(s)
- 1 or more CRTC(s)
- 1 or more Plane(s)

Finally, If you have a display connected, then one of your `Connector`s should read `Status: connected`. This `Connector` will also list many `Modes` that
the display can use.

#### Troubleshooting

If `drm-info` does *not* produce results similar to those above, then this
indicates that your kernel **is NOT yet ready** to support the GPU. This
means that kernel enablement is required before Ubuntu Frame can be enabled.

### Driver support check

Finally, we will need to check that you have the proper userspace drivers
installed on the system, namely:

- `EGL` (`libEGL.so.1`)
- `GLES` (`libGLESv2.so.2`)
- `GBM` (`libgbm.so.1`)

While *Mir* can be enabled on platforms that do not support `GBM/KMS`, this
requires a platform implementation to be written in *Mir*. This work is out
of the scope of this guide.

## How to assemble a `gpu-2404` provider snap

With the prerequisites out of the way, it is time to assemble a `gpu-2404` snap.

The snap itself must provide the libraries described in
[this document](https://ubuntu.com/frame/docs/24/how-to/use-snap-graphics/#supported-apis).
Unless you are incredibly space-constrained or in an unusual circumstance, it is a good idea to
provide **all** of these libraries. Even if your board doesn’t support these libraries, a failure to include
them will result in applications that would otherwise fall back to a supported API failing to start.

`libEGL`, `libGLESv2` and `libgbm` should be provided by the vendor's drivers.
`libvulkan` *may* be provided by the vendor as well.
The remaining packages should be available in the Ubuntu archive.

Most of these libraries do **NOT** need to be in a fixed location in the snap.
However, the following paths **MUST** be provided in a fixed location by your snap:

1. `X11`:
   ```yaml
   parts:
     x11:
       ...
       organize:
         # Expected at /X11 by the `gpu-2404` interface
         usr/share/X11: X11
       ...
   ```

The remainder of the paths will be established using the required script at the location
`bin/gpu-2404-provider-wrapper`. This script will be invoked with
`gpu-2404-provider-wrapper $EXECUTABLE $ARGS...`. This script does the
following:

1. It exports any environment variables that are required for the binary
   to find and use the vendor drivers
1. It then executes the provided `$EXECUTABLE` with `$ARGS…`.

An example of a script that provides `mesa-2404` is as follows:

```sh
# Derived from:
# https://github.com/canonical/mesa-2404/blob/main/scripts/bin/gpu-2404-provider-wrapper.in


#!/bin/bash
set -euo pipefail


# Find the parent directory of the wrapper script. This will be
# the path of the provider snap in the client. Your files with
# be found at a path relative to this path.
SELF="$( cd -- "$(dirname "$0")/.." ; pwd -P )/usr"


# The arch triplet(s) for this driver (e.g.: arm64-linux-gnu, i386-linux-gnu, etc)
ARCH_TRIPLETS=( x86_64-linux-gnu )


# VDPAU_DRIVER_PATH only supports a single path, rely on LD_LIBRARY_PATH instead
for arch in ${ARCH_TRIPLETS[@]}; do
    LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}${SELF}/lib/${arch}:${SELF}/lib/${arch}/vdpau
    LIBGL_DRIVERS_PATH=${LIBGL_DRIVERS_PATH:+$LIBGL_DRIVERS_PATH:}${SELF}/lib/${arch}/dri/
    LIBVA_DRIVERS_PATH=${LIBVA_DRIVERS_PATH:+$LIBVA_DRIVERS_PATH:}${SELF}/lib/${arch}/dri/
done

__EGL_VENDOR_LIBRARY_DIRS=${__EGL_VENDOR_LIBRARY_DIRS:+$__EGL_VENDOR_LIBRARY_DIRS:}${SELF}/share/glvnd/egl_vendor.d
__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS=${__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:+$__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:}${SELF}/share/egl/egl_external_platform.d
DRIRC_CONFIGDIR=$( cd -- "$(dirname "$0")/../drirc.d" ; pwd -P )
VK_LAYER_PATH=${VK_LAYER_PATH:+$VK_LAYER_PATH:}${SELF}/share/vulkan/implicit_layer.d/:${SELF}/share/vulkan/explicit_layer.d/
XDG_DATA_DIRS=${XDG_DATA_DIRS:+$XDG_DATA_DIRS:}${SELF}/share
XLOCALEDIR=${SELF}/share/X11/locale


# These are in the default LD_LIBRARY_PATH, but in case the snap dropped it inadvertently
if [ -d "/var/lib/snapd/lib/gl" ] && [[ ! ${LD_LIBRARY_PATH} =~ (^|:)/var/lib/snapd/lib/gl(:|$) ]]; then
    LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/var/lib/snapd/lib/gl
fi

if [ -d "/var/lib/snapd/lib/glvnd/egl_vendor.d" ]; then
    # This needs to be prepended, as glvnd goes depth-first on these
    __EGL_VENDOR_LIBRARY_DIRS=/var/lib/snapd/lib/glvnd/egl_vendor.d:${__EGL_VENDOR_LIBRARY_DIRS}
fi

if [ -d "/var/lib/snapd/lib/vulkan/icd.d" ]; then
    XDG_DATA_DIRS=${XDG_DATA_DIRS}:/var/lib/snapd/lib
fi

if [ -d "/var/lib/snapd/lib/gl/vdpau" ]; then
    LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/var/lib/snapd/lib/gl/vdpau
fi

export GBM_BACKENDS_PATH
export LD_LIBRARY_PATH
# Mesa-specific environment variable pointing to directory containing $VENDOR_dri.so files
export LIBGL_DRIVERS_PATH
export LIBVA_DRIVERS_PATH
# glvnd-specific environment variable pointing to directory containing ICD descriptor files
export __EGL_VENDOR_LIBRARY_DIRS
# EGL external platform interface specific environment
export __EGL_EXTERNAL_PLATFORM_CONFIG_DIRS
export DRIRC_CONFIGDIR
export VK_LAYER_PATH
export XDG_DATA_DIRS
export XLOCALEDIR

exec "$@"
```

This script has exported the proper paths via environment variables so that
any snap that connects to our provider snap can properly resolve these
libraries.

### Multiarch support

If the vendor drivers provide libraries for multiple sub-architectures (for example, 32-bit and 64-bit ARM) your provider snap can supply both. In this case, you will need to include all provided arch triples in the `ARCH_TRIPLETS` array in the above script (for example: `ARCH_TRIPLETS=( armhf-linux-gnu arm64-linux-gnu )`), and will need to set a few more environment variables in the `gpu-2404-provider-wrapper`.
An example from `mesa-2404` (which provides both `x86_64-linux-gnu` and `i386-linux-gnu`) adds these lines:

```sh
# /bin/gpu-2404-provider-wrapper

...
if [ "$SNAP_ARCH" == "amd64" ]; then
    GCONV_PATH=${GCONV_PATH:+$GCONV_PATH:}${SELF}/lib/i386-linux-gnu/gconv
fi

if [ -d "/var/lib/snapd/lib/gl32" ] && [[ ! ${LD_LIBRARY_PATH} =~ (^|:)/var/lib/snapd/lib/gl32(:|$) ]]; then
    LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/var/lib/snapd/lib/gl32
fi

if [ -d "/var/lib/snapd/lib/gl32/vdpau" ]; then
    LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/var/lib/snapd/lib/gl32/vdpau
fi

...

[ -z ${GCONV_PATH+x} ] || export GCONV_PATH
```

The `snapcraft.yaml` file will also need to be updated to point at the 32 bit paths.
You may find an example of using `i386` in
[mesa-2404](https://github.com/canonical/mesa-2404/blob/9d64e64fb06f7052f9e6f8a8899cac763f5fad7e/snap/snapcraft.yaml#L176).

### How to supply the vendor libraries

The following example supplies drivers from `libmali`. Using a parameter for
`make`, the libraries will be installed to `$SNAPCRAFT_PART_INSTALL/usr/lib`.
These libraries do not need to be in a particular path.

```yaml
parts:
  ...
  vendor-drivers:
  plugin: make
  source: libmali
  make-parameters:
    - LIBDIR=$SNAPCRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR
  prime:
    - usr/lib
  ...

...
slots:
 gpu-2404:
   interface: content
   read:
     - [$SNAP]
```

### How to supply the remaining libraries

As mentioned before, you should be able to get the remaining libraries from
the Ubuntu archive. The part below can supply these libraries, and they may be
organized into any directory, in this case they are in the same path as would
be found in a classic Ubuntu system. Here is an example from the `mesa-2404`
[snapcraft.yaml](https://github.com/canonical/mesa-2404/blob/main/snap/snapcraft.yaml).

```yaml
parts:
  drm:
    # DRM userspace
    #   o libdrm.so.2
    plugin: nil
    stage-packages:
      - libdrm2
      - libdrm-common
    organize:
      # Expected at /libdrm by the `gpu-2404` interface
      usr/share/libdrm: libdrm
    prime:
      - usr/lib
      - usr/share/doc/*/copyright
      - libdrm

  va:
    # Video Acceleration API
    #   o libva.so.2
    plugin: nil
    stage-packages:
      - libva2
      - libva-drm2
      - libva-x11-2
      - libva-wayland2
    prime:
      - usr/lib
      - usr/share/doc/*/copyright

  dri:
    # Userspace drivers
    plugin: nil
    stage-packages:
      - libgl1-mesa-dri
      - va-driver-all
      - vdpau-driver-all
      - libvdpau-va-gl1
      - mesa-vulkan-drivers
      - libglx-mesa0
    organize:
      # Expected at /drirc.d by the `gpu-2404` interface
      usr/share/drirc.d: drirc.d
    prime:
      - usr/lib
      - usr/share/doc/*/copyright
      - usr/share/vulkan
      - drirc.d
    override-stage: |
      # Strip any absolute paths out of the Vulkan ICD driver manifests;
      # the driver `.so` files will not be in a fixed location, so must use
      # relative paths and rely on `LD_LIBRARY_PATH` (set by the gpu-2404-proivder-wrapper script)
      # to find the driver `.so`s
      sed -i 's@/usr/lib/[a-z0-9_-]\+/@@' ${CRAFT_PART_INSTALL}/usr/share/vulkan/*/*.json
      craftctl default
      craftctl set version=$( apt-cache policy libgl1-mesa-dri | sed -rne 's/^\s+Candidate:\s+([^-]*)-.+$/\1/p' )

  x11:
    # X11 support (not much cost to having this)
    #   o libGLX.so.0
    plugin: nil
    stage-packages:
      - libglx0
      - libx11-xcb1
      - libxau6
      - libxcb-dri2-0
      - libxcb-dri3-0
      - libxcb-present0
      - libxcb-sync1
      - libxcb-xfixes0
      - libxcb1
      - libxdamage1
      - libxdmcp6
      - libxshmfence1
    organize:
      # Expected at /X11 by the `gpu-2404` interface
      usr/share/X11: X11
    prime:
      - usr/lib
      - usr/share/doc/*/copyright
      - X11

  wayland:
    # Wayland support (not much cost to having this)
    plugin: nil
    stage-packages:
      - libwayland-client0
      - libwayland-cursor0
      - libwayland-egl1
      - libwayland-server0
      - libnvidia-egl-wayland1
    prime:
      - usr/lib
      - usr/share/doc/*/copyright
      - usr/share/egl/egl_external_platform.d
```

When you're ready, build your snap using `snapcraft`.

### Troubleshooting

#### Vendor drivers require libraries newer than 2404 provides

Some vendor driver binaries might be built against newer libraries than are provided in the
Ubuntu 24.04 repositories. Particularly, the core `libwayland` libraries may introduce new
features since 24.04 that vendor binaries might require. This will manifest as missing
symbol errors at runtime.

Generally, new version requirements for libraries can be handled by adding a snapcraft
part building the required library version. An example of this for `libwayland` is:

```yaml
parts:
 ...
 wayland:
   plugin: meson
   meson-parameters:
     - -Ddocumentation=false
     - -Dtests=false
     - --prefix=$SNAPCRAFT_PART_INSTALL/usr
   build-packages:
     - libxml2-dev
     - libffi-dev
     - libexpat1-dev
     - pkg-config
   override-build: |
     snapcraftctl build
   source: https://gitlab.freedesktop.org/wayland/wayland.git
   source-type: git
   source-tag: 1.21.0
   prime:
     - usr/lib
```

## How to test a `gpu-2404` provider snap

Now we have our `gpu-2404` provider snap built. However, we don't yet know if it works
properly. Testing this snap is our next task.

Assuming that you have already installed the `graphics-test-tools` snap,
connect the snap that you've built to `graphics-test-tools`:

```shell
sudo snap disconnect graphics-test-tools:gpu-2404
sudo snap connect graphics-test-tools:gpu-2404 <your-snap>:gpu-2404
```

With that installed, we can begin testing.

### Use `eglinfo` to test

This is the most basic test that can be performed, and is a good indication of baseline GPU
driver setup. This should list _at least_ a GBM platform with the expected vendor and
`OpenGL_ES` client API. An abbreviated example (on a `mesa-2404` system):

```text
$ graphics-test-tools.eglinfo
EGL client extensions string:
	EGL_EXT_device_base EGL_EXT_device_enumeration EGL_EXT_device_query
	EGL_EXT_platform_base EGL_KHR_client_get_all_proc_addresses
	EGL_EXT_client_extensions EGL_KHR_debug EGL_EXT_platform_device
	EGL_EXT_platform_wayland EGL_KHR_platform_wayland
	EGL_EXT_platform_x11 EGL_KHR_platform_x11 EGL_EXT_platform_xcb
	EGL_MESA_platform_gbm EGL_KHR_platform_gbm
	EGL_MESA_platform_surfaceless


GBM platform:
EGL API version: 1.5
EGL vendor string: Mesa Project
EGL version string: 1.5
EGL client APIs: OpenGL OpenGL_ES
EGL driver name: iris
EGL extensions string:
	EGL_ANDROID_blob_cache EGL_ANDROID_native_fence_sync
	EGL_EXT_buffer_age EGL_EXT_create_context_robustness
	EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers
	EGL_IMG_context_priority EGL_KHR_cl_event2 EGL_KHR_config_attribs
	EGL_KHR_context_flush_control EGL_KHR_create_context
	EGL_KHR_create_context_no_error EGL_KHR_fence_sync
	EGL_KHR_get_all_proc_addresses EGL_KHR_gl_colorspace
	EGL_KHR_gl_renderbuffer_image EGL_KHR_gl_texture_2D_image
	EGL_KHR_gl_texture_3D_image EGL_KHR_gl_texture_cubemap_image
	EGL_KHR_image EGL_KHR_image_base EGL_KHR_image_pixmap
	EGL_KHR_no_config_context EGL_KHR_reusable_sync
	EGL_KHR_surfaceless_context EGL_EXT_pixel_format_float
	EGL_KHR_wait_sync EGL_MESA_configless_context EGL_MESA_drm_image
	EGL_MESA_image_dma_buf_export EGL_MESA_query_driver
	EGL_WL_bind_wayland_display
Configurations:
 	bf lv colorbuffer dp st  ms	vis   cav bi  renderable  supported
  id sz  l  r  g  b  a th cl ns b	id   eat nd gl es es2 vg surfaces
---------------------------------------------------------------------
0x01 32  0 10 10 10  2  0  0  0 0 0x30335241--     	y  y  y 	win
0x02 32  0 10 10 10  2 16  0  0 0 0x30335241--     	y  y  y 	win
0x03 32  0 10 10 10  2 24  0  0 0 0x30335241--     	y  y  y 	win
0x04 32  0 10 10 10  2 24  8  0 0 0x30335241--     	y  y  y 	win
0x05 32  0 10 10 10  2  0  0  2 1 0x30335241--     	y  y  y 	win
… lots more configurations, then other platform details …
```

If `eglinfo` does **NOT** list a GBM platform, or generates errors then you want to look
at {ref}`#troubleshooting`. If `eglinfo` does list a GBM platform
then we can proceed to testing `ubuntu-frame`.

### Test if `ubuntu-frame` works

First, install `ubuntu-frame`:

```shell
sudo snap install ubuntu-frame --devmode
```

Next, disconnect it from the default graphics interface:

```shell
sudo snap disconnect ubuntu-frame:gpu-2404
```

Then, connect it to your new provider snap:

```shell
sudo snap connect ubuntu-frame:gpu-2404 <your-snap>:gpu-2404
```

Finally, run `ubuntu-frame`:

```shell
ubuntu-frame
```

If everything previously has gone correctly this should result in `ubuntu-frame` coming
up on the connected outputs. If not, the log messages will hopefully help identify issues.

## How to test the confinement of your *Mir*-based snap

Once you have your `gpu-2404` provider snap setup, and you are confident that it works
well, you'll want to make sure that you can run the snap that *depends on* your provider snap
in a confined mode. Since we already have `ubuntu-frame` setup, we will use it as an example.
However, you should be able to run these tests on any *Mir*-based snap.

First, let's uninstall `ubuntu-frame` if it is installed:

```shell
sudo snap remove ubuntu-frame
```

Next, we can install `ubuntu-frame` without the `--devmode` flag and
connect it to the `gpu-2404` slot of your provider snap:

```shell
sudo snap install ubuntu-frame
sudo snap disconnect ubuntu-frame:gpu-2404
sudo snap connect ubuntu-frame:gpu-2404 <your-snap>:gpu-2404
```

Finally, we will run `ubuntu-frame` like before:

```shell
ubuntu-frame
```

If everything works, then `ubuntu-frame` is ready. If not, we'll have to troubleshoot.

(#troubleshooting)=

### Troubleshooting

When bringing up a confined ubuntu-frame snap on a new board with new drivers there are two
separate access control mechanisms:

- AppArmor
- The devices cgroup

#### AppArmor

The first is relatively easy to debug. When AppArmor would deny access to a resource, it outputs a nice
message in `dmesg`:

```
apparmor="DENIED" operation="open" profile="snap.ubuntu-frame.ubuntu-frame" name="/run/udev/data/c189:1" <other stuff>
```

You can find the AppArmor profile at `/var/lib/snapd/apparmor/profiles/snap.ubuntu-frame.ubuntu-frame`.
Make modifications to the profile using your favorite text editor. Finally, apply the change to replace
the default confinement:

```shell
sudo apparmor_parser -r /var/lib/snapd/apparmor/profiles/snap.ubuntu-frame.ubuntu-frame
```

**Warning**: Please note that this profile is regenerated automatically by many `snapd` actions.
As such, the modifications that you just made are *temporary*. To make them permanent,
you will need to upstream the fix against `snapd`.

#### The devices cgroup

The cgroup confinement is less easy to debug. The kernel emits no logs when the devices cgroup
denies access to a device node. The device node on the filesystem will appear to have the correct
permissions (and, for example, you will be able to touch it), but calls to `open()` will fail
with `EPERM`. If everything seems to be set up correctly, and there are no errors in `dmesg`,
but `Mir` is failing it’s likely that the devices cgroup confinement is to blame.

Additionally, it is more difficult to test as the cgroup is only set up during snap run startup,
and so the knobs we need to twiddle only exist while the snap is running.

We can get around this by running:

```shell
snap run --shell ubuntu-frame
```

This command will get snapd to do all the initialization and drop us into a shell.
Once the shell exists, there will be two(?!) cgroup folders found under `/sys/fs/cgroup/devices`:

1. /sys/fs/cgroup/devices/system.slice/snap.ubuntu-frame.ubuntu-frame.\$\{UUID}.scope, and
1. /sys/fs/cgroup/devices/snap.ubuntu-frame.ubuntu-frame

It is (2) that we’re after. You can check that you’ve got the right directory, because the
`devices.list` file will contain a bunch of lines like:

```
c 5:1 rwm
c 5:2 rwm
c 136:* rwm
c 137:* rwm
c 138:* rwm
```

You now need to give the cgroup permission to access the necessary devices, for which you need
the `major:minor` of the device nodes. You can find that with ls:

```
$ ls -la /sys/fs/cgroup/devices/snap.ubuntu-frame.ubuntu-frame
crw-rw-rw- 1 root root 10, 60 Jan 10 04:56 /dev/mali0
```

Here we see that the `/dev/mali0` device node has `major:minor` equal to `10:60`.

Now we can enable access to the relevant device node, via:

```shell
echo "c 10:60 rw" | sudo tee /sys/fs/cgroup/devices/snap.ubuntu-frame.ubuntu-frame/devices.allow
```

And now we can go back to the shell, and try running `ubuntu-frame`:

<!-- pyml disable-num-lines 3 commands-show-output -->

```shell
$SNAP/usr/local/bin/frame
```
