Skip to content

docs(roadmap): browser compositor complete — cross-platform GPU shari… #176

docs(roadmap): browser compositor complete — cross-platform GPU shari…

docs(roadmap): browser compositor complete — cross-platform GPU shari… #176

Workflow file for this run

name: Visual
on:
push:
branches: [master, main]
pull_request:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
# Compile the whole workspace on each desktop OS. This build-verifies the
# per-OS platform backends (today: X11 on Linux; the others use the headless
# fallback until their native backends land).
build:
name: Build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build workspace
run: cargo build --workspace --all-targets
# Run the windowed example against a real (virtual) X server and screenshot
# the result, proving the native X11 backend connects, maps a window, and
# paints. The Forma X11 backend speaks the wire protocol directly, so no
# libX11/libxcb is needed — only Xvfb (a server) and ImageMagick (capture).
visual-x11:
name: Visual test (X11 / Xvfb)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, x11-utils, and a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick x11-utils fonts-dejavu-core
- name: Build the window example
run: cargo build --release -p window
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Launch the Forma window and screenshot it
env:
DISPLAY: ":99"
FORMA_X11_DEBUG: "1"
run: |
mkdir -p visual_output
./target/release/window > visual_output/app.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
echo "=== window tree ==="
xwininfo -root -tree -display :99 | head -40 || true
# Capture the whole virtual root (our window maps at the top-left).
import -window root visual_output/forma-x11.png
echo "=== capture ==="; identify visual_output/forma-x11.png
echo "=== app.log ==="; cat visual_output/app.log || true
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshot + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-x11-screenshot
path: visual_output/*
if-no-files-found: warn
# Embedded-content viewport (browser-compositor seam): the viewportdemo
# reserves a viewport rect and the app composites CPU-rendered content (a
# cyan/magenta checkerboard standing in for a content process's GPU surface)
# into it. Screenshot the window and assert the viewport region shows that
# content (blue-dominant, multi-color) rather than the dark placeholder, then
# click inside it and confirm the press was forwarded to the content sink with
# viewport-local coordinates (input forwarding).
visual-viewport:
name: Visual test (Viewport / X11)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, xdotool, x11-utils, a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick xdotool x11-utils fonts-dejavu-core
- name: Build the viewport demo
run: cargo build --release -p viewportdemo
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Launch the viewport demo and check the composited content
env:
DISPLAY: ":99"
run: |
mkdir -p visual_output
./target/release/viewportdemo > visual_output/viewport.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
import -window root visual_output/forma-viewport.png
echo "=== capture ==="; identify visual_output/forma-viewport.png
echo "=== viewport.log ==="; cat visual_output/viewport.log || true
# The window is 480x360 at the root top-left; the 320x240 viewport is
# centered under the heading, so a region around the window center
# (240,180) lands well inside it. Crop 200x150 there.
convert visual_output/forma-viewport.png -crop 200x150+140+105 +repage \
visual_output/viewport-region.png
# The checkerboard is cyan (0,255,255) + magenta (255,0,255): both have
# full blue, so the region's mean blue is ~1.0 — unreachable by the dark
# theme background (~0.1) or the slate placeholder (~0.17).
mb=$(convert visual_output/viewport-region.png -format "%[fx:mean.b]" info:)
echo "viewport region mean.b=$mb"
awk "BEGIN{exit !($mb > 0.7)}"
# ...and it must be actual content (a checker), not a single flat color.
colors=$(convert visual_output/viewport-region.png -format '%k' info:)
echo "distinct colors in region: $colors"
[ "$colors" -gt 1 ]
# Input forwarding: click inside the viewport (window center 240,180)
# and confirm the app forwarded a viewport-local pointer press to the
# content sink (the demo logs each forwarded ViewportEvent).
xdotool mousemove 240 180 click 1
sleep 1
echo "=== viewport.log after click ==="; cat visual_output/viewport.log || true
grep -q "viewport input:.*PointerDown" visual_output/viewport.log
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshot + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-viewport-screenshot
path: visual_output/*
if-no-files-found: warn
# Scroll containers: a tall list clipped to a fixed-height viewport, wheel-
# scrolled with xdotool. Screenshot before/after and assert the visible frame
# changed (content moved) — proving offset + clip work end to end.
visual-scroll:
name: Visual test (Scroll / X11)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, xdotool, x11-utils, a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick xdotool x11-utils fonts-dejavu-core
- name: Build the scroll demo
run: cargo build --release -p scrolldemo
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Scroll the list and screenshot before/after
env:
DISPLAY: ":99"
run: |
mkdir -p visual_output
./target/release/scrolldemo > visual_output/scroll.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
import -window root visual_output/scroll-before.png
# Hover over the viewport so the wheel targets the scroll container,
# then wheel down several notches (X11 button 5 = scroll down).
xdotool mousemove 210 200
sleep 0.5
for _ in 1 2 3 4 5 6 7 8; do xdotool click 5; done
sleep 1
import -window root visual_output/scroll-after.png
echo "=== scroll.log ==="; cat visual_output/scroll.log || true
# The visible frame must have changed (the list scrolled within its clip).
diff=$(compare -metric AE visual_output/scroll-before.png visual_output/scroll-after.png null: 2>&1 || true)
echo "changed pixels: $diff"
[ "${diff%%[.e]*}" -gt 2000 ]
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshots + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-scroll-screenshot
path: visual_output/*
if-no-files-found: warn
# Overlays: a dropdown menu and a modal dialog drawn above the main tree.
# xdotool opens each; the dropdown must change the frame, and the modal must
# darken it (the scrim) — proving the overlay layer + hit-routing work.
visual-overlay:
name: Visual test (Overlays / X11)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, xdotool, x11-utils, a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick xdotool x11-utils fonts-dejavu-core
- name: Build the overlay demo
run: cargo build --release -p overlaydemo
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Open a dropdown and a modal, asserting the overlay layer paints
env:
DISPLAY: ":99"
run: |
mkdir -p visual_output
./target/release/overlaydemo > visual_output/overlay.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
import -window root visual_output/overlay-base.png
# Open the dropdown (Menu button center ~ (96,56)).
xdotool mousemove 96 56 click 1
sleep 1
import -window root visual_output/overlay-menu.png
# Dismiss by pressing empty space (the non-modal catcher).
xdotool mousemove 440 360 click 1
sleep 0.5
# Open the modal dialog (Dialog button center ~ (228,56)).
xdotool mousemove 228 56 click 1
sleep 1
import -window root visual_output/overlay-dialog.png
echo "=== overlay.log ==="; cat visual_output/overlay.log || true
# The dropdown must have changed the frame...
menudiff=$(compare -metric AE visual_output/overlay-base.png visual_output/overlay-menu.png null: 2>&1 || true)
echo "menu changed pixels: $menudiff"
[ "${menudiff%%[.e]*}" -gt 1500 ]
# ...and the modal's scrim must darken the whole frame.
mb=$(convert visual_output/overlay-base.png -colorspace Gray -format "%[fx:mean]" info:)
mdg=$(convert visual_output/overlay-dialog.png -colorspace Gray -format "%[fx:mean]" info:)
echo "mean brightness base=$mb dialog=$mdg"
awk "BEGIN{exit !($mdg < $mb)}"
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshots + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-overlay-screenshot
path: visual_output/*
if-no-files-found: warn
# Clipboard: copy a field's text and paste it back, doubling it. Verifies the
# in-app copy/paste path (Ctrl+A/C/V) via a before/after screenshot diff, and
# the OS round-trip by reading the X11 CLIPBOARD selection back with xclip.
visual-clipboard:
name: Visual test (Clipboard / X11)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, xdotool, xclip, a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick xdotool xclip fonts-dejavu-core
- name: Build the clipboard demo
run: cargo build --release -p clipboarddemo
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Copy + paste in the field and check the OS selection
env:
DISPLAY: ":99"
run: |
mkdir -p visual_output
./target/release/clipboarddemo > visual_output/clipboard.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
xdotool key Tab # focus the field ("Clip")
sleep 0.5
import -window root visual_output/clip-before.png
xdotool key ctrl+a # select all
xdotool key ctrl+c # copy → clipboard + OS selection
sleep 0.5
xdotool key End # collapse selection, caret at end
xdotool key ctrl+v # paste → "ClipClip"
sleep 1
import -window root visual_output/clip-after.png
echo "=== clipboard.log ==="; cat visual_output/clipboard.log || true
# The paste must have changed the field (text doubled). The added
# "Clip" glyphs are thin strokes — ~170 differing pixels — whereas a
# failed paste only nudges the caret (<20), so 60 cleanly separates.
diff=$(compare -metric AE visual_output/clip-before.png visual_output/clip-after.png null: 2>&1 || true)
echo "changed pixels: $diff"
[ "${diff%%[.e]*}" -gt 60 ]
# The copy must have populated the X11 CLIPBOARD selection with "Clip"
# (the X11 backend owns the selection and serves SelectionRequest).
sel=$(xclip -o -selection clipboard 2>/dev/null || true)
echo "X11 CLIPBOARD = '$sel'"
[ "$sel" = "Clip" ]
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshots + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-clipboard-screenshot
path: visual_output/*
if-no-files-found: warn
# True OS multi-window: the multiwindow example opens two real top-level X11
# windows (one parent App, two panes over shared state) side by side, a red
# one at x=0 and a blue one at x=340. Screenshot the root and confirm BOTH
# painted — the left region is red-dominant and the right region blue-dominant.
visual-multiwindow:
name: Visual test (Multi-window / X11)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick fonts-dejavu-core
- name: Build the multi-window demo
run: cargo build --release -p multiwindow
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Open two windows and screenshot the root
env:
DISPLAY: ":99"
run: |
mkdir -p visual_output
./target/release/multiwindow > visual_output/multiwindow.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
import -window root visual_output/multiwindow.png
echo "=== multiwindow.log ==="; cat visual_output/multiwindow.log || true
# Crop a patch well inside each window: A at 0,0 and B at 340,0
# (both 320x240).
convert visual_output/multiwindow.png -crop 280x180+20+20 +repage visual_output/a.png
convert visual_output/multiwindow.png -crop 280x180+360+20 +repage visual_output/b.png
# Command substitution (not `read`, whose non-zero EOF status would
# trip `set -e` since ImageMagick's info: output has no trailing NL).
a=$(convert visual_output/a.png -format "%[fx:mean.r] %[fx:mean.b]" info:)
b=$(convert visual_output/b.png -format "%[fx:mean.r] %[fx:mean.b]" info:)
ar=${a% *}; ab=${a#* }
br=${b% *}; bb=${b#* }
echo "window A region: mean.r=$ar mean.b=$ab"
echo "window B region: mean.r=$br mean.b=$bb"
# A must be red-dominant and B blue-dominant — proving both windows
# exist, side by side, each painting its own pane.
awk "BEGIN{exit !($ar > $ab && $bb > $br)}"
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshots + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-multiwindow-screenshot
path: visual_output/*
if-no-files-found: warn
# The calculator demo end to end: render the button grid, then click 7 + 8 =
# via xdotool and confirm the display region changed (0 -> 15). Button centers
# are computed from the fixed 300x460 layout (16px panel padding, 76px display,
# 60px rows with 8px gaps, four 61px columns with 8px gaps). The arithmetic
# itself is covered by the example's unit tests in the main CI test job.
visual-calculator:
name: Visual test (Calculator / X11)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, xdotool, a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick xdotool fonts-dejavu-core
- name: Build the calculator demo
run: cargo build --release -p calculator
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Render, click 7 + 8 =, and check the display changed
env:
DISPLAY: ":99"
run: |
mkdir -p visual_output
./target/release/calculator > visual_output/calculator.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
import -window root visual_output/calc-before.png
# Button centers (logical px == physical at 1x; window at 0,0):
# columns ~x: 46, 115, 184, 253 rows ~y: 134(C..),202(7..),
# 270(4..),338(1..),406(0/./=)
xdotool mousemove 46 202 click 1 # 7
sleep 0.3
xdotool mousemove 253 338 click 1 # +
sleep 0.3
xdotool mousemove 115 202 click 1 # 8
sleep 0.3
xdotool mousemove 253 406 click 1 # =
sleep 0.6
import -window root visual_output/calc-after.png
echo "=== calculator.log ==="; cat visual_output/calculator.log || true
# Crop the display strip (x16..284, y16..92) from both and require it to
# have changed: "0" became "15".
convert visual_output/calc-before.png -crop 268x76+16+16 +repage visual_output/disp-before.png
convert visual_output/calc-after.png -crop 268x76+16+16 +repage visual_output/disp-after.png
diff=$(compare -metric AE visual_output/disp-before.png visual_output/disp-after.png null: 2>&1 || true)
echo "display changed pixels: $diff"
[ "${diff%%[.e]*}" -gt 40 ]
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshots + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-calculator-screenshot
path: visual_output/*
if-no-files-found: warn
# Tabs + right-click context menu: render the tabsdemo, click the "Settings"
# tab (the body swatch recolors), then right-click the body and confirm a
# context menu appears. Each interaction must change the frame.
visual-tabs:
name: Visual test (Tabs + context menu / X11)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, xdotool, a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick xdotool fonts-dejavu-core
- name: Build the tabs demo
run: cargo build --release -p tabsdemo
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Switch tabs and open a context menu
env:
DISPLAY: ":99"
run: |
mkdir -p visual_output
./target/release/tabsdemo > visual_output/tabs.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
import -window root visual_output/tabs-0.png
# Click the middle tab ("Settings"): tabs sit ~y32, three across a
# 360px window — centers near x69 / x180 / x291.
xdotool mousemove 180 32 click 1
sleep 0.6
import -window root visual_output/tabs-1.png
# Right-click the body to open the context menu.
xdotool mousemove 180 250 click 3
sleep 0.6
import -window root visual_output/tabs-2.png
echo "=== tabs.log ==="; cat visual_output/tabs.log || true
sw=$(compare -metric AE visual_output/tabs-0.png visual_output/tabs-1.png null: 2>&1 || true)
mn=$(compare -metric AE visual_output/tabs-1.png visual_output/tabs-2.png null: 2>&1 || true)
echo "tab-switch changed pixels: $sw"
echo "context-menu changed pixels: $mn"
# Switching tabs recolors a big swatch (large change); opening the menu
# paints a panel of items over the body (also large).
[ "${sw%%[.e]*}" -gt 500 ]
[ "${mn%%[.e]*}" -gt 500 ]
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshots + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-tabs-screenshot
path: visual_output/*
if-no-files-found: warn
# Render a real window whose frames are produced on the GPU (forma-gpu) and
# presented through the X11 Surface — the GPU render path wired into the live
# on-screen present loop. Runs under Xvfb + Mesa (surfaceless EGL).
visual-gpu-window:
name: Visual test (GPU window / X11)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, Mesa GL/EGL (+ dev symlinks), ImageMagick, x11-utils, a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick x11-utils fonts-dejavu-core \
libegl-dev libgles-dev libegl-mesa0 libgl1-mesa-dri libgbm-dev
- name: Build the GPU window example (GLES backend)
run: cargo build --release -p gpuwindow --features forma-gpu/gl
- name: Start Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
- name: Launch the GPU window and screenshot it
env:
DISPLAY: ":99"
LIBGL_ALWAYS_SOFTWARE: "1"
GALLIUM_DRIVER: llvmpipe
run: |
mkdir -p visual_output
./target/release/gpuwindow > visual_output/gpuwindow.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 5
import -window root visual_output/forma-gpu-window.png
echo "=== capture ==="; identify visual_output/forma-gpu-window.png
echo "=== gpuwindow.log ==="; cat visual_output/gpuwindow.log || true
# The GPU path must actually be used (no software fallback message)...
! grep -q "GPU render unavailable" visual_output/gpuwindow.log
# ...and the window must have painted a non-trivial frame (the dark
# theme background plus the card): more than one distinct color.
colors=$(convert visual_output/forma-gpu-window.png -format '%k' info:)
echo "distinct colors: $colors"
[ "$colors" -gt 1 ]
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload screenshot + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-gpu-window-screenshot
path: visual_output/*
if-no-files-found: warn
# Cross-process GPU surface seam (browser content path): export a GL texture as
# a dma-buf and re-import it. Build-verified always; functionally verified only
# where the device supports the dma-buf EGL extensions. CI runs software Mesa
# (llvmpipe), which often lacks dma-buf export — so the demo reports UNSUPPORTED
# (exit 2) there and that is accepted; a real failure (exit 1) is not. The full
# functional pass is expected on real GPU hardware.
gpu-dmabuf:
name: GPU dma-buf export/import (Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Mesa GL/EGL
run: |
sudo apt-get update
sudo apt-get install -y libegl-dev libgles-dev libegl-mesa0 libgl1-mesa-dri libgbm-dev
- name: Build the dma-buf spike (GLES backend)
run: cargo build --release -p dmabuftest --features forma-gpu/gl
- name: Run the export/import self-test
env:
LIBGL_ALWAYS_SOFTWARE: "1"
GALLIUM_DRIVER: llvmpipe
run: |
set +e
./target/release/dmabuftest | tee dmabuf.log
rc=${PIPESTATUS[0]}
echo "exit code: $rc"
# 0 = functional pass, 2 = unsupported on this (software) device — both
# acceptable in CI; 1 = extensions present but pixels wrong (a real bug).
if [ "$rc" -eq 1 ]; then
echo "dma-buf round-trip produced wrong pixels"
exit 1
fi
if [ "$rc" -ne 0 ] && [ "$rc" -ne 2 ]; then
echo "unexpected exit code $rc"
exit 1
fi
# Present extension negotiation over the raw X11 socket. The Present extension
# (which flips the DRI3 pixmap to the window) needs no GPU, so dri3probe can
# negotiate it under Xvfb — the CI-verifiable half of the zero-copy GPU-present
# path (the dma-buf Pixmap it flips still needs real GPU hardware). DRI3 itself
# is absent under Xvfb (no DRM), so the probe exits non-zero; we assert Present.
dri3-present:
name: DRI3 Present (X11 negotiation / Xvfb)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb
run: sudo apt-get update && sudo apt-get install -y xvfb
- name: Build dri3probe
run: cargo build --release -p dri3probe
- name: Negotiate the Present extension under Xvfb
run: |
Xvfb :99 -screen 0 1024x768x24 &
XVFB_PID=$!
sleep 2
set +e
DISPLAY=:99 ./target/release/dri3probe | tee dri3.log
set -e
kill "$XVFB_PID" 2>/dev/null || true
# The Present extension must negotiate (Xvfb supports Present, no GPU).
grep -qE "Present probe: Present [0-9]+\.[0-9]+ available" dri3.log
# Content-process compositing over the CPU shm path (the dual of the GPU
# dma-buf path): the UI process spawns a separate content process that renders
# into a memfd and passes the fd over a Unix socket (SCM_RIGHTS); the UI process
# maps the same memory, composites it into a viewport, and forwards input back,
# which the content process applies by redrawing into the shared buffer. The
# demo self-checks the whole loop — headless and GPU-free, so it runs anywhere.
content-process:
name: Content process (shm IPC + compositing)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build the content-process demo
run: cargo build --release -p contentproc
- name: Run the cross-process content path
run: |
# Capture both streams: the UI process logs to stdout, the content
# process to stderr.
./target/release/contentproc 2>&1 | tee contentproc.log
# The UI process mapped the content process's buffer, composited it into
# a viewport, and the forwarded input crossed the process boundary.
grep -q "RESULT: PASS" contentproc.log
# ...and the content process dropped privilege: its seccomp sandbox
# blocks opening new sockets while its existing IPC fd keeps working.
grep -q "content: sandbox active (new sockets blocked)" contentproc.log
# Render the window example under a headless wlroots compositor (sway) and
# screenshot it with grim, proving the hand-authored Wayland backend connects,
# binds the globals, creates an xdg-shell window, and presents via wl_shm.
visual-wayland:
name: Visual test (Wayland / sway)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install sway, grim, wtype, seatd, ImageMagick, a font
run: |
sudo apt-get update
sudo apt-get install -y sway grim wtype seatd imagemagick fonts-dejavu-core
- name: Build the window + textinput examples
run: cargo build --release -p window -p textinput
- name: Run under headless sway and screenshot with grim
run: |
export XDG_RUNTIME_DIR="$RUNNER_TEMP/xdgrt"
mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR"
# Headless wlroots with software (pixman) rendering, no GPU or input.
export WLR_BACKENDS=headless
export WLR_RENDERER=pixman
export WLR_LIBINPUT_NO_DEVICES=1
# seatd provides a seat without logind; allow this user's group.
sudo seatd -g "$(id -gn)" > seatd.log 2>&1 &
sleep 1
export LIBSEAT_BACKEND=seatd
printf 'output HEADLESS-1 resolution 1024x768\nexec sleep 600\n' > sway.cfg
sway -c sway.cfg > sway.log 2>&1 &
SWAY_PID=$!
sleep 6
SOCK=$(ls "$XDG_RUNTIME_DIR" | grep -m1 '^wayland-[0-9]*$' || true)
echo "wayland socket: '$SOCK'"
export WAYLAND_DISPLAY="$SOCK"
export FORMA_WAYLAND_DEBUG=1
mkdir -p visual_output
# --- render: the window example, screenshotted with grim ---
./target/release/window > visual_output/wl.log 2>&1 &
WIN_PID=$!
sleep 5
grim visual_output/forma-wayland.png || echo "grim failed"
kill "$WIN_PID" 2>/dev/null || true
sleep 1
# --- input: type into the textinput field via wtype (virtual
# keyboard). Tab focuses the field, then we type text — exercising the
# wl_seat keyboard + xkb keymap decode path end to end. ---
./target/release/textinput > visual_output/wl-input.log 2>&1 &
TXT_PID=$!
sleep 4
# One wtype session keeps a single virtual keyboard alive for the whole
# sequence (Tab to focus, then type), so the keymap stays valid: Tab,
# then "forma wl", with a delay between keystrokes.
wtype -d 120 -k Tab -- "forma wl" 2>> visual_output/wtype.log; sleep 2
grim visual_output/forma-wayland-input.png || echo "grim failed"
echo "=== wtype.log ==="; cat visual_output/wtype.log || true
kill "$TXT_PID" 2>/dev/null || true
echo "=== seatd.log ==="; cat seatd.log || true
echo "=== sway.log ==="; cat sway.log || true
echo "=== wl.log ==="; cat visual_output/wl.log || true
echo "=== wl-input.log ==="; cat visual_output/wl-input.log || true
identify visual_output/forma-wayland.png || true
kill "$SWAY_PID" 2>/dev/null || true
- name: Upload screenshot + logs
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-wayland-screenshot
path: |
visual_output/*
sway.log
seatd.log
if-no-files-found: warn
# Verify *real* native input: launch a "click counter" window under Xvfb,
# click it with xdotool, and screenshot before/after to confirm the
# native-event → dispatch → re-present loop works end to end.
interaction-x11:
name: Interaction test (X11 / xdotool)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Xvfb, ImageMagick, xdotool, and a font
run: |
sudo apt-get update
sudo apt-get install -y xvfb imagemagick xdotool fonts-dejavu-core
- name: Build the clickdemo + textinput + hoverdemo + textarea examples
# Release so per-event re-rasterization keeps up with input.
run: cargo build --release -p clickdemo -p textinput -p hoverdemo -p textarea
- name: Click + type, screenshotting the results
env:
DISPLAY: ":99"
FORMA_X11_DEBUG: "1"
run: |
mkdir -p visual_output
Xvfb :99 -screen 0 1024x768x24 &
echo "XVFB_PID=$!" >> "$GITHUB_ENV"
sleep 2
# --- pointer: click a counting button ---
./target/release/clickdemo > visual_output/clickdemo.log 2>&1 &
CLICK_PID=$!
sleep 3
import -window root visual_output/clicks-before.png
xdotool mousemove 320 240 click 1; sleep 1
xdotool mousemove 320 240 click 1; sleep 1
import -window root visual_output/clicks-after.png
kill "$CLICK_PID" 2>/dev/null || true
echo "=== clickdemo.log ==="; cat visual_output/clickdemo.log || true
# --- keyboard: focus the field (Tab), type, then move the caret and
# insert mid-string to exercise the caret-aware EditBuffer ---
./target/release/textinput > visual_output/textinput.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 3
xdotool key Tab; sleep 1
xdotool type --delay 80 "Forma"
# Caret now after "Forma"; move left twice and insert -> "ForXYma".
xdotool key Left Left; sleep 0.5
xdotool type --delay 80 "XY"
sleep 2
import -window root visual_output/text-typed.png
# Pointer drag-selection: press inside the field near the text start and
# drag right to select the leading characters, then screenshot.
xdotool mousemove 46 90 mousedown 1; sleep 0.3
xdotool mousemove 96 90; sleep 0.3
xdotool mouseup 1; sleep 1
import -window root visual_output/text-dragselect.png
# Keyboard selection: Home, Shift+End to select the whole line.
xdotool key Home; sleep 0.3
xdotool key shift+End; sleep 1
import -window root visual_output/text-selected.png
echo "=== textinput.log ==="; cat visual_output/textinput.log || true
kill "$APP_PID" 2>/dev/null || true
# --- hover: move the cursor onto the right button ---
./target/release/hoverdemo > visual_output/hoverdemo.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 3
xdotool mousemove 284 70 # center of the right button
sleep 1
import -window root visual_output/hover.png
kill "$APP_PID" 2>/dev/null || true
# --- multi-line editing: type across lines (Enter = newline), then
# navigate up and select down across lines ---
./target/release/textarea > visual_output/textarea.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 3
xdotool key Tab; sleep 1
xdotool type --delay 60 "Line one"
xdotool key Return
xdotool type --delay 60 "Line two"
xdotool key Return
xdotool type --delay 60 "Line three"
sleep 1
import -window root visual_output/textarea-typed.png
# Move up two lines to the start, then select down across two lines.
xdotool key Up Up Home; sleep 0.3
xdotool key shift+Down shift+Down; sleep 1
import -window root visual_output/textarea-select.png
echo "=== textarea.log ==="; cat visual_output/textarea.log || true
- name: Stop the app and Xvfb
if: always()
run: |
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
[ -n "$XVFB_PID" ] && kill "$XVFB_PID" 2>/dev/null || true
- name: Upload before/after screenshots
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-x11-interaction
path: visual_output/*
if-no-files-found: warn
# Run the windowed example on a real Windows desktop (the runner has one) and
# capture the screen to confirm the native Win32 backend creates a window and
# blits the framebuffer.
visual-windows:
name: Visual test (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build the window example
run: cargo build --release -p window
- name: Exercise the raw Direct3D 11 (WARP) backend
shell: bash
run: |
cargo run --release -p d3ddemo --features forma-gpu/d3d | tee d3ddemo.log
# WARP is always available on the runner, so a device must come up.
grep -q "D3D11 device: WARP device" d3ddemo.log
- name: Exercise the raw UI Automation provider
shell: bash
run: |
cargo run --release -p uiademo | tee uiademo.log
# The hand-built IRawElementProviderSimple answers via its COM vtable:
# a server-side provider (options 1), a Group control (50026), "Forma".
grep -q 'UIA provider: options=1 controltype=50026 name="Forma"' uiademo.log
# ...and a GPU-rendered frame must read back as the cleared forma blue
# (R=0x60 G=0x9c B=0xff A=0xff), checked tool-free from the printed pixel.
grep -q "D3D11 readback: .* first pixel 96,156,255,255" d3ddemo.log
# ...and a triangle drawn by HLSL shaders must put forma green
# (52,211,153) at the center pixel.
grep -q "D3D11 triangle: .* center pixel 52,211,153,255" d3ddemo.log
# ...and the shared-texture export (dma-buf analog) must run to
# completion — its line appears whether WARP grants the shared handle or
# declines it; absence would mean the COM call crashed.
grep -q "D3D11 shared handle" d3ddemo.log
- name: Launch the Forma window and screenshot the desktop
shell: powershell
run: |
New-Item -ItemType Directory -Force -Path visual_output | Out-Null
$proc = Start-Process -FilePath ".\target\release\window.exe" -PassThru
Start-Sleep -Seconds 5
Add-Type -AssemblyName System.Windows.Forms, System.Drawing
$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
$bmp = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
$gfx = [System.Drawing.Graphics]::FromImage($bmp)
$gfx.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
$bmp.Save("$PWD\visual_output\forma-windows.png")
Write-Host "Saved screenshot ($($bounds.Width)x$($bounds.Height))"
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
- name: Upload screenshot
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-windows-screenshot
path: visual_output/*.png
if-no-files-found: warn
# Render the same UI under several themes and montage them into one image,
# showing the theme engine + customization builder.
visual-themes:
name: Visual test (Themes)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install ImageMagick + font
run: sudo apt-get update && sudo apt-get install -y imagemagick fonts-dejavu-core
- name: Build the themegallery example
run: cargo build --release -p themegallery
- name: Render themes and montage them
run: |
mkdir -p visual_output
./target/release/themegallery
for i in 0 1 2 3; do
convert -size 360x300 -depth 8 "rgba:theme$i.raw" "t$i.png"
done
montage t0.png t1.png t2.png t3.png -tile 2x2 -geometry +6+6 -background '#222' \
visual_output/forma-themes.png
identify visual_output/forma-themes.png
- name: Upload screenshot
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-themes-screenshot
path: visual_output/*.png
if-no-files-found: warn
# Build the web (wasm) target and screenshot it rendering in headless Chrome,
# proving the canvas present path (putImageData of the RGBA framebuffer).
visual-web:
name: Visual test (Web / wasm)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: Build the wasm module
run: cargo build -p forma-web --target wasm32-unknown-unknown --release
- name: Install a font
run: sudo apt-get update && sudo apt-get install -y fonts-dejavu-core
- name: Assemble the web bundle
run: |
mkdir -p web visual_output
cp crates/forma-web/web/index.html web/
cp target/wasm32-unknown-unknown/release/forma_web.wasm web/
cp /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf web/font.ttf
ls -l web/
- name: Serve + screenshot with headless Chrome
run: |
python3 -m http.server 8000 --directory web &
echo "SRV_PID=$!" >> "$GITHUB_ENV"
sleep 2
# virtual-time-budget lets the wasm load, the font fetch, and the
# self-driven clicks run before the screenshot is taken.
google-chrome --headless=new --no-sandbox --disable-gpu --hide-scrollbars \
--window-size=700,420 --virtual-time-budget=15000 \
--screenshot="$PWD/visual_output/forma-web.png" \
http://localhost:8000/index.html
echo "Captured:"; identify visual_output/forma-web.png || true
- name: Stop server
if: always()
run: '[ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null || true'
- name: Upload screenshot
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-web-screenshot
path: visual_output/*.png
if-no-files-found: warn
# Draw a WebGPU triangle (hand-written WGSL, no wgpu) in headless Chrome on
# SwiftShader and screenshot it, proving the browser GPU backend.
visual-webgpu:
name: Visual test (WebGPU / Chrome)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install ImageMagick
run: sudo apt-get update && sudo apt-get install -y imagemagick
- name: Serve + render the WebGPU triangle with headless Chrome
run: |
mkdir -p visual_output
python3 -m http.server 8000 --directory crates/forma-web/web &
echo "SRV_PID=$!" >> "$GITHUB_ENV"
sleep 2
# The page reads the rendered pixel back in JS and logs it, so the check
# is deterministic (no canvas-composite/screenshot timing). The triangle
# center must be forma green (52,211,153).
#
# Bringing up WebGPU on the bundled SwiftShader Vulkan ICD headlessly is
# itself intermittently flaky on CI (Chrome occasionally blocklists the
# adapter on a cold start), so retry the *render* a few times — a
# transient adapter miss on one launch usually succeeds on the next. A
# genuine, persistent failure still fails the job.
ok=0
for attempt in 1 2 3 4 5; do
echo "=== WebGPU attempt $attempt ==="
# WebGPU via Chrome's Dawn on SwiftShader. NB: not --disable-gpu.
google-chrome --headless=new --no-sandbox \
--enable-unsafe-webgpu --enable-features=Vulkan \
--use-angle=swiftshader --use-vulkan=swiftshader \
--enable-logging=stderr --v=1 \
--window-size=420,300 --virtual-time-budget=20000 \
--screenshot="$PWD/visual_output/forma-webgpu.png" \
http://localhost:8000/webgpu.html 2>chrome.log || true
grep -iE "WEBGPU |WEBGPU_" chrome.log | head -5 || true
if grep -q "WEBGPU center=52,211,153" chrome.log; then
ok=1; break
fi
sleep 3
done
identify visual_output/forma-webgpu.png || true
[ "$ok" = 1 ]
- name: Stop server
if: always()
run: '[ -n "$SRV_PID" ] && kill "$SRV_PID" 2>/dev/null || true'
- name: Upload screenshot
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-webgpu-screenshot
path: visual_output/*.png
if-no-files-found: warn
# Route a Forma frame through the GPU (forma-gpu: EGL + GLES2) on Mesa's
# software GL and screenshot the read-back, proving the GPU present path.
visual-gpu:
name: Visual test (GPU / GLES via Mesa)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install Mesa (software GL/EGL + Vulkan lavapipe), ImageMagick, font
run: |
sudo apt-get update
sudo apt-get install -y \
libegl-dev libgles-dev libegl-mesa0 libgl1-mesa-dri libgbm-dev \
libvulkan-dev libvulkan1 mesa-vulkan-drivers vulkan-tools \
imagemagick fonts-dejavu-core
- name: Build gpudemo with the GL + Vulkan features
run: cargo build --release -p gpudemo --features forma-gpu/gl,forma-gpu/vk
- name: Render through the GPU and convert the readback to PNG
env:
LIBGL_ALWAYS_SOFTWARE: "1"
GALLIUM_DRIVER: llvmpipe
run: |
mkdir -p visual_output
./target/release/gpudemo | tee gpudemo.log
convert -size 420x300 -depth 8 rgba:gpu-out.raw visual_output/forma-gpu.png
# GPU-native geometry: solid rects tessellated + filled on the GPU.
convert -size 420x300 -depth 8 rgba:gpu-rects.raw visual_output/forma-gpu-rects.png
# GPU-native render of the actual widget-tree Scene (render_scene).
convert -size 420x300 -depth 8 rgba:gpu-scene.raw visual_output/forma-gpu-scene.png
# Vulkan-rendered frame (clear) read back from the GPU, raw FFI.
convert -size 420x300 -depth 8 rgba:gpu-vk.raw visual_output/forma-gpu-vk.png
# Vulkan triangle drawn by real SPIR-V shaders, read back, raw FFI.
convert -size 420x300 -depth 8 rgba:gpu-vk-tri.raw visual_output/forma-gpu-vk-tri.png
echo "Captured:"; identify visual_output/forma-gpu*.png
# The raw-FFI Vulkan foundation must reach a device (Mesa lavapipe) and
# create a logical device + graphics queue.
grep -q "Vulkan devices:" gpudemo.log
grep -q "Vulkan init: logical device" gpudemo.log
# ...and allocate a DEVICE_LOCAL color image (offscreen render target).
grep -q "Vulkan image: .* color image bound" gpudemo.log
# ...and build the render-pass + framebuffer over an image view.
grep -q "Vulkan framebuffer: .* framebuffer + render pass" gpudemo.log
# ...and actually submit a clearing render pass, fence-synced (real GPU work).
grep -q "Vulkan clear: .*(fence signaled)" gpudemo.log
# ...and read a GPU-rendered frame back to the CPU (a real screenshot).
grep -q "Vulkan readback: .* bytes" gpudemo.log
# The readback must be the cleared forma blue (R=0x60 G=0x9c B=0xff), not black.
vkpx=$(convert visual_output/forma-gpu-vk.png -format '%[pixel:p{10,10}]' info:)
echo "Vulkan readback pixel: $vkpx"
echo "$vkpx" | grep -q "96,156,255"
# The triangle was drawn by real shaders: the center pixel must be forma
# green (R=0x34 G=0xd3 B=0x99 = 52,211,153), the corner the dark clear.
grep -q "Vulkan triangle: .* bytes" gpudemo.log
tript=$(convert visual_output/forma-gpu-vk-tri.png -format '%[pixel:p{210,150}]' info:)
echo "Vulkan triangle center pixel: $tript"
echo "$tript" | grep -q "52,211,153"
- name: Upload screenshot
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-gpu-screenshot
path: visual_output/*.png
if-no-files-found: warn
# Verify macOS input: click the centered clickdemo window with `cliclick`
# and screenshot before/after. (Synthetic input on a CI runner can be blocked
# by the accessibility permission model; treated as best-effort.)
interaction-macos:
name: Interaction test (macOS / cliclick)
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install cliclick
run: brew install cliclick
- name: Build the clickdemo example (release)
run: cargo build --release -p clickdemo
- name: Click the window and screenshot before/after
run: |
mkdir -p visual_output
./target/release/clickdemo > visual_output/clickdemo-macos.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 4
screencapture -x visual_output/mac-clicks-before.png
bounds=$(osascript -e 'tell application "Finder" to get bounds of window of desktop')
w=$(echo "$bounds" | awk -F', ' '{print $3}')
h=$(echo "$bounds" | awk -F', ' '{print $4}')
echo "screen ${w}x${h}; clicking center"
cliclick "c:$((w/2)),$((h/2))"; sleep 1
cliclick "c:$((w/2)),$((h/2))"; sleep 1
screencapture -x visual_output/mac-clicks-after.png
echo "=== clickdemo-macos.log ==="; cat visual_output/clickdemo-macos.log || true
- name: Stop the app
if: always()
run: '[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true'
- name: Upload screenshots
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-macos-interaction
path: visual_output/*
if-no-files-found: warn
# Run the windowed example on the macOS runner's desktop and screenshot it
# with the built-in `screencapture`, confirming the native Cocoa backend
# creates a window and blits the framebuffer.
visual-macos:
name: Visual test (macOS)
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build the window example
run: cargo build --release -p window
- name: Exercise the raw Metal backend
run: |
cargo run --release -p metaldemo --features forma-gpu/mtl | tee metaldemo.log
# A Metal device is always available on the macOS runner.
grep -q "Metal device: " metaldemo.log
# ...and a GPU-rendered frame must read back as the cleared forma blue
# (R=0x60 G=0x9c B=0xff A=0xff), checked tool-free from the printed pixel.
grep -q "Metal readback: .* first pixel 96,156,255,255" metaldemo.log
# ...and a triangle drawn by a compiled .metal shader must put forma
# green (52,211,153) at the center pixel.
grep -q "Metal triangle: .* center pixel 52,211,153,255" metaldemo.log
# ...and a shareable IOSurface (dma-buf analog) must be created with a
# global id — the macOS runner has real IOSurface support.
grep -q "Metal IOSurface: IOSurface .* id [0-9]" metaldemo.log
- name: Launch the Forma window and screenshot the screen
env:
FORMA_COCOA_A11Y: "1"
run: |
mkdir -p visual_output
./target/release/window > visual_output/macos.log 2>&1 &
echo "APP_PID=$!" >> "$GITHUB_ENV"
sleep 5
screencapture -x visual_output/forma-macos.png
echo "Captured:"; (sips -g pixelWidth -g pixelHeight visual_output/forma-macos.png || true)
echo "=== macos.log ==="; cat visual_output/macos.log || true
# The custom NSView vends NSAccessibility info via real objc dispatch:
# an accessible AXGroup labelled with the window title ("Forma").
grep -q "Cocoa a11y: element=true role=AXGroup label=Forma" visual_output/macos.log
- name: Stop the app
if: always()
run: '[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true'
- name: Upload screenshot
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-macos-screenshot
path: visual_output/*.png
if-no-files-found: warn
# Connect to a private session bus with the hand-written D-Bus client (the
# AT-SPI accessibility-bridge foundation) and confirm the SASL handshake +
# Hello assign us a unique name. No display needed.
a11y-dbus:
name: a11y (D-Bus bridge / Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install D-Bus + AT-SPI core
run: sudo apt-get update && sudo apt-get install -y dbus at-spi2-core
- name: Build the a11y demo
run: cargo build --release -p a11ydemo
- name: Connect to the session + AT-SPI buses and read the unique names
run: |
dbus-run-session -- ./target/release/a11ydemo | tee a11y.log
# The session bus assigns every connection a ":1.N" unique name...
grep -qE "D-Bus connected: unique name :1\." a11y.log
# ...and we can reach the separate AT-SPI bus via org.a11y.Bus.
grep -qE "AT-SPI bus connected: unique name :1\." a11y.log
- name: Expose a Forma UI's accessibility tree over AT-SPI and query it
run: |
dbus-run-session -- bash -ec '
./target/release/a11ydemo serve > serve.log 2>&1 &
SRV=$!
sleep 2
cat serve.log
# We must own the well-known name (RequestName -> 1 = primary owner).
grep -q "RequestName org.formaui.A11yDemo -> 1" serve.log
# The root of the Forma UI is a Window -> AT-SPI FRAME role (27),
# with two children (a label and a button).
grep -q "a11y root: role=27 .* children=2" serve.log
# Query our hand-written AT-SPI server the way a screen reader would.
dbus-send --session --print-reply --dest=org.formaui.A11yDemo \
/org/formaui/a11y org.a11y.atspi.Accessible.GetRole | tee role.log
grep -q "uint32 27" role.log
dbus-send --session --print-reply --dest=org.formaui.A11yDemo \
/org/formaui/a11y org.freedesktop.DBus.Properties.Get \
string:org.a11y.atspi.Accessible string:ChildCount | tee cc.log
grep -q "int32 2" cc.log
dbus-send --session --print-reply --dest=org.formaui.A11yDemo \
/org/formaui/a11y org.freedesktop.DBus.Properties.Get \
string:org.a11y.atspi.Accessible string:Name | tee name.log
grep -q "Forma" name.log
# ...and the standard introspection still works.
dbus-send --session --print-reply --dest=org.formaui.A11yDemo \
/org/formaui/a11y org.freedesktop.DBus.Introspectable.Introspect | tee introspect.log
grep -q "org.a11y.atspi.Accessible" introspect.log
kill $SRV 2>/dev/null || true
'
# Native file dialog over the xdg-desktop-portal FileChooser. No real portal
# backend exists headlessly, so the demo ships a mock portal (owns
# org.freedesktop.portal.Desktop, answers OpenFile, emits the Response signal
# with a canned URI). The client calls dialog::open_file and must receive that
# path — exercising the full hand-written D-Bus FileChooser client round-trip.
file-dialog-portal:
name: File dialog (xdg portal / Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install D-Bus
run: sudo apt-get update && sudo apt-get install -y dbus
- name: Build the file-dialog demo
run: cargo build --release -p filedialog
- name: Round-trip OpenFile through the mock portal
run: |
dbus-run-session -- bash -ec '
./target/release/filedialog serve > serve.log 2>&1 &
SRV=$!
sleep 2
out=$(./target/release/filedialog)
echo "client: $out"
cat serve.log
# The mock portal served exactly one request...
grep -q "mock portal served one request" serve.log
# ...and the client got the URI it returned, decoded to a path.
[ "$out" = "PICKED: /tmp/forma-pick.txt" ]
kill $SRV 2>/dev/null || true
'
# Run the native iOS UIKit backend on a booted simulator and confirm it boots,
# builds the window, and renders a frame — a real runtime check of the backend
# the cross-compile job only build-verifies.
visual-ios:
name: Visual test (iOS simulator)
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-ios-sim
- uses: Swatinem/rust-cache@v2
- name: Build the window example for the iOS simulator
run: cargo build --release -p window --target aarch64-apple-ios-sim
- name: Bundle, boot a simulator, launch, and capture the runtime marker
run: |
mkdir -p visual_output Forma.app
cp target/aarch64-apple-ios-sim/release/window Forma.app/window
cat > Forma.app/Info.plist <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleExecutable</key><string>window</string>
<key>CFBundleIdentifier</key><string>com.karpeleslab.forma</string>
<key>CFBundleName</key><string>Forma</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>LSRequiresIPhoneOS</key><true/>
<key>UIDeviceFamily</key><array><integer>1</integer></array>
<key>MinimumOSVersion</key><string>15.0</string>
</dict></plist>
PLIST
# Use an iPhone simulator the runner already provisioned for its
# installed runtime (avoids device/runtime incompatibility from picking
# an arbitrary device type).
DEV=$(xcrun simctl list devices available -j | python3 -c "import sys,json;d=json.load(sys.stdin)['devices'];ids=[x['udid'] for rt,devs in d.items() if 'iOS' in rt for x in devs if x.get('isAvailable') and 'iPhone' in x['name']];print(ids[0] if ids else '')")
echo "device=$DEV"
[ -n "$DEV" ]
xcrun simctl boot "$DEV" || true
xcrun simctl bootstatus "$DEV" -b || true
xcrun simctl install "$DEV" Forma.app
# Launch the app (it runs forever) and give it a moment to render.
xcrun simctl launch "$DEV" com.karpeleslab.forma || true
sleep 12
xcrun simctl io "$DEV" screenshot visual_output/forma-ios.png || true
(sips -g pixelWidth -g pixelHeight visual_output/forma-ios.png || true)
# The backend writes a runtime marker into the app's tmp dir; read it
# back from the simulator's data container (app stdout isn't reliably
# captured headlessly).
DATA=$(xcrun simctl get_app_container "$DEV" com.karpeleslab.forma data)
echo "data container: $DATA"
ls -la "$DATA/tmp" || true
cat "$DATA/tmp/forma-ios.txt" || true
# The native UIKit backend booted via UIApplicationMain, created the
# window, and the Forma handler rendered a non-empty frame.
grep -qE "Forma iOS: window shown, framebuffer [1-9][0-9]*x[1-9][0-9]*" "$DATA/tmp/forma-ios.txt"
- name: Upload screenshot + log
uses: actions/upload-artifact@v4
if: always()
with:
name: forma-ios-screenshot
path: visual_output/*.png
if-no-files-found: warn
# Run the native Android backend on an emulator: build the cdylib, package a
# debug APK by hand (aapt + zipalign + apksigner — no Gradle), install it, and
# confirm via logcat that the NativeActivity presented a Forma frame to the
# ANativeWindow surface.
visual-android:
name: Visual test (Android emulator)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-linux-android
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Build the cdylib and assemble a signed debug APK
run: |
set -x
NDK="${ANDROID_NDK_LATEST_HOME:-$ANDROID_NDK_HOME}"
TC="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="$TC/x86_64-linux-android21-clang"
cargo build -p androiddemo --target x86_64-linux-android --release
BT=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1)
JAR=$(ls "$ANDROID_HOME"/platforms/*/android.jar | sort -V | tail -1)
echo "build-tools=$BT android.jar=$JAR ndk=$NDK"
cat > AndroidManifest.xml <<'MAN'
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.karpeleslab.forma">
<uses-sdk android:minSdkVersion="24" android:targetSdkVersion="34"/>
<application android:label="Forma" android:hasCode="false" android:debuggable="true">
<activity android:name="android.app.NativeActivity" android:exported="true">
<meta-data android:name="android.app.lib_name" android:value="forma_android"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
MAN
mkdir -p lib/x86_64
cp target/x86_64-linux-android/release/libforma_android.so lib/x86_64/
"$BT/aapt" package -f -M AndroidManifest.xml -I "$JAR" -F base.apk
"$BT/aapt" add base.apk lib/x86_64/libforma_android.so
keytool -genkeypair -keystore debug.ks -storepass android -keypass android \
-alias d -dname "CN=forma" -keyalg RSA -keysize 2048 -validity 10000
"$BT/zipalign" -f -p 4 base.apk aligned.apk
"$BT/apksigner" sign --ks debug.ks --ks-pass pass:android --key-pass pass:android \
--out forma.apk aligned.apk
echo "APK built:"; ls -l forma.apk
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Launch on emulator and check logcat
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: google_apis
force-avd-creation: true
disable-animations: true
script: |
adb install -r forma.apk
adb logcat -c
adb shell am start -n com.karpeleslab.forma/android.app.NativeActivity
sleep 15
adb logcat -d > logcat.txt || true
grep "Forma Android: window" logcat.txt || true
grep -q "Forma Android: window [0-9]*x[0-9]* presented=true" logcat.txt