Linux Graphics Stack on the Arduino UNO Q

This will probably be the last post in this thread with new content. My experiments with pure Weston programming were more driven by curiosity than by practical reasons.

In this step, I will use the Firefox browser within Weston and use WebGPU for my line performance test. The browser adds another abstraction layer to the workflow, since it takes care of the direct interaction with Weston and the other low-level layers.

WebGPU is a modern web API that provides direct access to the GPU from within the browser. It is designed as the successor of WebGL and allows more efficient and flexible use of graphics hardware. Compared to WebGL, it gives lower-level control over GPU resources and better performance, especially for compute-heavy tasks. WebGPU is based on modern graphics concepts similar to Vulkan, Metal, or Direct3D 12, and can be used not only for rendering but also for general-purpose GPU computations.

To make this work, it is important to use a recent Firefox version (>=147), because WebGPU support is only properly available there.

On Debian-based systems, you can install the latest Firefox version using the Mozilla repository. I prefer doing this manually with nano instead of scripts:

First, create the keyring directory:

sudo install -d -m 0755 /etc/apt/keyrings

Download the Mozilla signing key:

wget https://packages.mozilla.org/apt/repo-signing-key.gpg
sudo mv repo-signing-key.gpg /etc/apt/keyrings/packages.mozilla.org.asc

Then add the repository:

sudo nano /etc/apt/sources.list.d/mozilla.list

Insert:

deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main

Save with CTRL+O, ENTER and exit with CTRL+X.

Next, set the package priority:

sudo nano /etc/apt/preferences.d/mozilla

Insert:

Package: *
Pin: origin packages.mozilla.org
Pin-Priority: 1000

After this:

sudo apt update
sudo apt install firefox

We can start Firefox by just typing firefox into the Weston CLI. To check if everything is working properly we type the following in the URL bar:

about:config

Make sure:

dom.webgpu.enabled = true
gfx.webrender.all = true

Also check in:

about:support

That:

* Compositing = WebRender
* WebGPU = available / enabled

If everything is correct, WebGPU should work directly when starting Firefox inside Weston. Instead of the freedreno OpenGL ES driver webGPU uses the tulip Vulkan driver of the Adreno 702 GPU. And instead of C, we use HTML / JavaScript in the browser.

<!doctype html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <title>WebGPU Line Performance Test</title>
  <style>
    html, body {
      margin: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: black;
      color: white;
      font-family: monospace;
    }

    #gpuCanvas {
      display: block;
      width: 100vw;
      height: 100vh;
    }

    #overlay {
      position: fixed;
      left: 12px;
      top: 12px;
      padding: 8px 10px;
      background: rgba(0,0,0,0.55);
      white-space: pre;
      pointer-events: none;
    }
  </style>
</head>
<body>
  <canvas id="gpuCanvas"></canvas>
  <div id="overlay">Init...</div>

  <script type="module">
    const canvas = document.getElementById("gpuCanvas");
    const overlay = document.getElementById("overlay");

    const lineCount = 100000;
    const verticesPerLine = 2;
    const floatsPerVertex = 6; // x,y,r,g,b,a
    const totalVertices = lineCount * verticesPerLine;
    const totalFloats = totalVertices * floatsPerVertex;
    const vertexData = new Float32Array(totalFloats);

    function nowSeconds() {
      return performance.now() * 1e-3;
    }
    
    function nextRAF() {
      return new Promise(resolve => requestAnimationFrame(resolve));
    } 

    function resizeCanvasToDisplaySize(device, context, format) {
      const dpr = window.devicePixelRatio || 1;
      const width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
      const height = Math.max(1, Math.floor(canvas.clientHeight * dpr));

      if (canvas.width !== width || canvas.height !== height) {
        canvas.width = width;
        canvas.height = height;

        context.configure({
          device,
          format,
          alphaMode: "opaque",
        });
      }
    }

    function fillRandomLines(width, height) {
      const w1 = Math.max(1, width - 1);
      const h1 = Math.max(1, height - 1);

      for (let i = 0; i < lineCount; i++) {
        const x0 = (Math.random() * width) | 0;
        const y0 = (Math.random() * height) | 0;
        const x1 = (Math.random() * width) | 0;
        const y1 = (Math.random() * height) | 0;

        const fx0 =  2.0 * x0 / w1 - 1.0;
        const fy0 =  1.0 - 2.0 * y0 / h1;

        const fx1 =  2.0 * x1 / w1 - 1.0;
        const fy1 =  1.0 - 2.0 * y1 / h1;

        const r = Math.random();
        const g = Math.random();
        const b = Math.random();

        const base = i * verticesPerLine * floatsPerVertex;

        vertexData[base + 0]  = fx0;
        vertexData[base + 1]  = fy0;
        vertexData[base + 2]  = r;
        vertexData[base + 3]  = g;
        vertexData[base + 4]  = b;
        vertexData[base + 5]  = 1.0;

        vertexData[base + 6]  = fx1;
        vertexData[base + 7]  = fy1;
        vertexData[base + 8]  = r;
        vertexData[base + 9]  = g;
        vertexData[base + 10] = b;
        vertexData[base + 11] = 1.0;
      }
    }

    async function main() {
      if (!("gpu" in navigator)) {
        overlay.textContent = "WebGPU nicht verfügbar.";
        throw new Error("navigator.gpu not available");
      }

      const adapter = await navigator.gpu.requestAdapter({
        powerPreference: "high-performance",
      });

      if (!adapter) {
        overlay.textContent = "Kein WebGPU-Adapter gefunden.";
        throw new Error("No GPU adapter");
      }

      const device = await adapter.requestDevice();
      const context = canvas.getContext("webgpu");
      const format = navigator.gpu.getPreferredCanvasFormat();

      resizeCanvasToDisplaySize(device, context, format);
      window.addEventListener("resize", () => {
        resizeCanvasToDisplaySize(device, context, format);
      });

      const shaderModule = device.createShaderModule({
        code: `
struct VertexIn {
  @location(0) position : vec2<f32>,
  @location(1) color    : vec4<f32>,
};

struct VertexOut {
  @builtin(position) position : vec4<f32>,
  @location(0) color          : vec4<f32>,
};

@vertex
fn vs_main(v: VertexIn) -> VertexOut {
  var out: VertexOut;
  out.position = vec4<f32>(v.position, 0.0, 1.0);
  out.color = v.color;
  return out;
}

@fragment
fn fs_main(inFrag: VertexOut) -> @location(0) vec4<f32> {
  return inFrag.color;
}
        `
      });

      const pipeline = await device.createRenderPipelineAsync({
        layout: "auto",
        vertex: {
          module: shaderModule,
          entryPoint: "vs_main",
          buffers: [
            {
              arrayStride: 6 * 4,
              attributes: [
                {
                  shaderLocation: 0,
                  offset: 0,
                  format: "float32x2",
                },
                {
                  shaderLocation: 1,
                  offset: 2 * 4,
                  format: "float32x4",
                },
              ],
            },
          ],
        },
        fragment: {
          module: shaderModule,
          entryPoint: "fs_main",
          targets: [{ format }],
        },
        primitive: {
          topology: "line-list",
        },
      });

      const vertexBufferSize = vertexData.byteLength;

      const vertexBuffer = device.createBuffer({
        size: vertexBufferSize,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
      });

      let lastPrint = 0.0;

      async function frame() {
        resizeCanvasToDisplaySize(device, context, format);

        const t0 = nowSeconds();

        fillRandomLines(canvas.width, canvas.height);

        const t1 = nowSeconds();

        device.queue.writeBuffer(
          vertexBuffer,
          0,
          vertexData.buffer,
          vertexData.byteOffset,
          vertexData.byteLength
        );

        const encoder = device.createCommandEncoder();

        const pass = encoder.beginRenderPass({
          colorAttachments: [
            {
              view: context.getCurrentTexture().createView(),
              clearValue: { r: 0, g: 0, b: 0, a: 1 },
              loadOp: "clear",
              storeOp: "store",
            }
          ]
        });

        pass.setPipeline(pipeline);
        pass.setVertexBuffer(0, vertexBuffer);
        pass.draw(totalVertices, 1, 0, 0);
        pass.end();

        device.queue.submit([encoder.finish()]);
        const t2 = nowSeconds();

        await device.queue.onSubmittedWorkDone();
        const t3 = nowSeconds();

        await nextRAF();
        const t4 = nowSeconds();

        if (t4 - lastPrint > 0.25) {
          const createMs     = (t1 - t0) * 1000.0;
          const submitMs     = (t2 - t1) * 1000.0;
          const gpuDoneMs    = (t3 - t0) * 1000.0;
          const repaintMs    = (t4 - t0) * 1000.0;
          const estFpsGpu    = gpuDoneMs > 0 ? 1000.0 / gpuDoneMs : 0.0;
          const estFpsPaint  = repaintMs > 0 ? 1000.0 / repaintMs : 0.0;

          overlay.textContent =
            `Lines         : ${lineCount}\n` +
            `Vertices      : ${totalVertices}\n` +
            `Canvas        : ${canvas.width} x ${canvas.height}\n` +
            `Create Vert   : ${createMs.toFixed(3)} ms\n` +
            `Submit        : ${submitMs.toFixed(3)} ms\n` +
            `GPU done      : ${gpuDoneMs.toFixed(3)} ms\n` +
            `Next rAF      : ${repaintMs.toFixed(3)} ms\n` +
            `Est. FPS GPU  : ${estFpsGpu.toFixed(1)}\n` +
            `Est. FPS Paint: ${estFpsPaint.toFixed(1)}`;

          console.log(`Create Vert : ${createMs.toFixed(3)} ms`);
          console.log(`Submit      : ${submitMs.toFixed(3)} ms`);
          console.log(`GPU done    : ${gpuDoneMs.toFixed(3)} ms`);
          console.log(`Next rAF    : ${repaintMs.toFixed(3)} ms\n`);

          lastPrint = t4;
        }

        setTimeout(() => requestAnimationFrame(frame), 1000);
      }

      requestAnimationFrame(frame);
    }

    main().catch(err => {
      console.error(err);
      overlay.textContent = "Fehler:\n" + err.message;
    });
  </script>
</body>
</html>

The script realizes the 100,000 lines performance test. It initializes a GPU adapter and device via navigator.gpu , creates a WebGPU canvas context, and configures it with the preferred swapchain format. All low-level parts are handled by WebGPU. This significantly reduces the size of the script.

A shader module (WGSL) defines a simple vertex and fragment shader. The render pipeline is set up with a vertex buffer layout containing position and color attributes, and uses a line-list primitive topology.

For each frame, new vertex data is generated on the CPU and uploaded to a GPU buffer using device.queue.writeBuffer. A command encoder and render pass are created, the pipeline and vertex buffer are bound, and a draw call is issued. The commands are submitted to the GPU queue, and synchronization is done via onSubmittedWorkDone().

This setup shows the typical WebGPU workflow: device creation, pipeline definition, buffer management, command encoding, and submission to the GPU.

To run the script, a local web server is required, since WebGPU is only available in a secure context and does not work when opening the file directly. A simple way is to use a Python-based server:

python3 -m http.server 8000

Then open the browser and navigate to http://localhost:8000.

Pressing F11 switches Firefox to fullscreen mode:

The measurement results are rendered by the browser as an additional CSS layer.

0.306 s is a slight increase compared to plain Weston/Wayland (0.269 s). But keep in mind that the CPU code is running as JavaScript in the browser engine instead of compiled C code.

So here are the final results of the rudimentary test of drawing 100,000 random lines on a 1920 x 1080 display:

Single-thread CPU rendering on DRM/KMS      : 2.81 s
Multi-threaded CPU rendering on DRM/KMS     : 1.12 s
OpenGL ES GPU rendering on DRM/KMS          : 0.23 s
OpenGL ES GPU rendering on Wayland/Weston   : 0.27 s
Firefox/WebGPU rendering on Wayland/Weston  : 0.31 s

So summing it all up, I will proceed with Firefox/WebGPU. The high level of abstraction and ease of use is worth the slight performance impact compared to plain C code. In my applications, data is mainly transmitted via web protocols, which is a big advantage for a browser-based solution.